Browse Source

Merge pull request #6 from RuudBurger/develop

Develop
pull/5915/head
maxkoryukov 9 years ago
parent
commit
d792fa7d92
  1. 22
      couchpotato/core/media/_base/providers/nzb/newznab.py
  2. 2
      couchpotato/core/media/_base/providers/torrent/torrentbytes.py
  3. 34
      couchpotato/core/plugins/browser.py
  4. 3
      couchpotato/core/plugins/profile/main.py
  5. 23
      couchpotato/core/plugins/quality/main.py
  6. 5
      couchpotato/core/plugins/renamer.py
  7. 1
      couchpotato/core/plugins/scanner.py
  8. 2
      couchpotato/core/plugins/score/scores.py
  9. 49
      couchpotato/core/plugins/test_browser.py
  10. 140
      couchpotato/core/settings.py
  11. 64
      couchpotato/core/softchroot.py
  12. 54
      couchpotato/core/test_softchroot.py
  13. 13
      couchpotato/runner.py
  14. 54
      couchpotato/static/scripts/combined.base.min.js
  15. 17
      couchpotato/static/scripts/page/about.js
  16. 53
      couchpotato/static/scripts/page/settings.js
  17. 70
      couchpotato/static/style/combined.min.css
  18. 16
      couchpotato/static/style/settings.scss
  19. 3
      couchpotato/templates/index.html
  20. 2
      do.tests.sh
  21. 8
      libs/unrar2/__init__.py
  22. 57
      libs/unrar2/unix.py
  23. 39
      libs/unrar2/windows.py
  24. 8
      package.json

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

@ -72,9 +72,17 @@ class Base(NZBProvider, RSS):
detail_url = self.getTextElement(nzb, 'guid')
nzb_id = detail_url.split('/')[-1:].pop()
try:
link = self.getElement(nzb, 'enclosure').attrib['url']
except:
link = self.getTextElement(nzb, 'link')
if '://' not in detail_url:
detail_url = (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id)
if not link:
link = ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host)
if not name:
continue
@ -106,7 +114,7 @@ class Base(NZBProvider, RSS):
'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'url': link,
'detail_url': detail_url,
'content': self.getTextElement(nzb, 'description'),
'description': description,
@ -225,7 +233,7 @@ config = [{
'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://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
<a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
<a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>, <a href="https://www.usenet-crawler.com" target="_blank">Usenet-Crawler</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=',
'options': [
@ -236,30 +244,30 @@ config = [{
},
{
'name': 'use',
'default': '0,0,0,0,0'
'default': '0,0,0,0,0,0'
},
{
'name': 'host',
'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://api.nzbgeek.info,https://www.nzbfinder.ws',
'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://api.nzbgeek.info,https://www.nzbfinder.ws,https://www.usenet-crawler.com',
'description': 'The hostname of your newznab provider',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': '0,0,0,0,0',
'default': '0,0,0,0,0,0',
'description': 'Starting score for each release found via this provider.',
},
{
'name': 'custom_tag',
'advanced': True,
'label': 'Custom tag',
'default': ',,,,',
'default': ',,,,,',
'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org',
},
{
'name': 'api_key',
'default': ',,,,',
'default': ',,,,,',
'label': 'Api Key',
'description': 'Can be found on your profile page',
'type': 'combined',

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

@ -55,7 +55,7 @@ class Base(TorrentProvider):
link = cells[1].find('a', attrs = {'class': 'index'})
full_id = link['href'].replace('details.php?id=', '')
torrent_id = full_id[:6]
torrent_id = full_id[:7]
name = toUnicode(link.get('title', link.contents[0]).encode('ISO-8859-1')).strip()
results.append({

34
couchpotato/core/plugins/browser.py

@ -10,7 +10,7 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import sp, ss, toUnicode
from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.softchroot import SoftChroot
log = CPLog(__name__)
@ -33,6 +33,9 @@ autoload = 'FileBrowser'
class FileBrowser(Plugin):
def __init__(self):
soft_chroot_dir = self.conf('soft_chroot', section='core')
self.soft_chroot = SoftChroot(soft_chroot_dir)
addApiView('directory.list', self.view, docs = {
'desc': 'Return the directory list of a given directory',
'params': {
@ -78,11 +81,17 @@ class FileBrowser(Plugin):
return driveletters
def view(self, path = '/', show_hidden = True, **kwargs):
home = getUserDir()
if self.soft_chroot.enabled:
if not self.soft_chroot.is_subdir(home):
home = self.soft_chroot.chdir
if not path:
path = home
if path.endswith(os.path.sep):
path = path.rstrip(os.path.sep)
elif self.soft_chroot.enabled:
path = self.soft_chroot.add(path)
try:
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
@ -90,17 +99,34 @@ class FileBrowser(Plugin):
log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc()))
dirs = []
if self.soft_chroot.enabled:
dirs = map(self.soft_chroot.cut, dirs)
parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep):
parent = '/'
elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep
# TODO : check on windows:
is_root = path == '/'
if self.soft_chroot.enabled:
is_root = self.soft_chroot.is_root_abs(path)
# fix paths:
if self.soft_chroot.is_subdir(parent):
parent = self.soft_chroot.cut(parent)
else:
parent = os.path.sep
home = self.soft_chroot.cut(home)
return {
'is_root': path == '/',
'is_root': is_root,
'empty': len(dirs) == 0,
'parent': parent,
'home': home + os.path.sep,
'home': home,
'platform': os.name,
'dirs': dirs,
}

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

@ -206,6 +206,9 @@ class ProfilePlugin(Plugin):
'label': '3D HD',
'qualities': ['1080p', '720p'],
'3d': [True, True]
}, {
'label': 'UHD 4K',
'qualities': ['720p', '1080p', '2160p']
}]
# Create default quality profile

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

@ -23,6 +23,7 @@ class QualityPlugin(Plugin):
}
qualities = [
{'identifier': '2160p', 'hd': True, 'allow_3d': True, 'size': (10000, 650000), 'median_size': 20000, 'label': '2160p', 'width': 3840, 'height': 2160, 'alternative': [], 'allow': [], 'ext':['mkv'], 'tags': ['x264', 'h264', '2160']},
{'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'median_size': 40000, '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), 'median_size': 10000, 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264', '1080']},
{'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'median_size': 5500, 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264', '720']},
@ -65,6 +66,7 @@ class QualityPlugin(Plugin):
})
addEvent('app.initialize', self.fill, priority = 10)
addEvent('app.load', self.fillBlank, priority = 120)
addEvent('app.test', self.doTest)
@ -146,7 +148,18 @@ class QualityPlugin(Plugin):
'success': False
}
def fill(self):
def fillBlank(self):
db = get_db()
try:
existing = list(db.all('quality'))
if len(self.qualities) > len(existing):
log.error('Filling in new qualities')
self.fill(reorder = True)
except:
log.error('Failed filling quality database with new qualities: %s', traceback.format_exc())
def fill(self, reorder = False):
try:
db = get_db()
@ -156,7 +169,7 @@ class QualityPlugin(Plugin):
existing = None
try:
existing = db.get('quality', q.get('identifier'))
existing = db.get('quality', q.get('identifier'), with_doc = reorder)
except RecordNotFound:
pass
@ -179,6 +192,10 @@ class QualityPlugin(Plugin):
'finish': [True],
'wait_for': [0],
})
elif reorder:
log.info2('Updating quality order')
existing['doc']['order'] = order
db.update(existing['doc'])
order += 1
@ -493,6 +510,8 @@ class QualityPlugin(Plugin):
'Movie.Name.2014.720p.HDSCR.4PARTS.MP4.AAC.ReleaseGroup': {'size': 2401, 'quality': 'scr'},
'Movie.Name.2014.720p.BluRay.x264-ReleaseGroup': {'size': 10300, 'quality': '720p'},
'Movie.Name.2014.720.Bluray.x264.DTS-ReleaseGroup': {'size': 9700, 'quality': '720p'},
'Movie Name 2015 2160p SourceSite WEBRip DD5 1 x264-ReleaseGroup': {'size': 21800, 'quality': '2160p'},
'Movie Name 2012 2160p WEB-DL FLAC 5 1 x264-ReleaseGroup': {'size': 59650, 'quality': '2160p'}
}
correct = 0

5
couchpotato/core/plugins/renamer.py

@ -807,7 +807,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
# Match all found ignore files with the tag_files and return True found
for tag_file in tag_files:
for tag_file in [tag_files] if isinstance(tag_files,str) else tag_files:
ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
if ignore_file:
return True
@ -1228,6 +1228,9 @@ Remove it if you want it to be renamed (again, or at least let it try again)
log.error('Rar modify date enabled, but failed: %s', traceback.format_exc())
extr_files.append(extr_file_path)
del rar_handle
# Tag archive as extracted if no cleanup.
if not cleanup and os.path.isfile(extr_file_path):
self.tagRelease(release_download = {'folder': os.path.dirname(archive['file']), 'files': [archive['file']]}, tag = 'extracted')
except Exception as e:
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
continue

1
couchpotato/core/plugins/scanner.py

@ -74,6 +74,7 @@ class Scanner(Plugin):
}
resolutions = {
'2160p': {'resolution_width': 3840, 'resolution_height': 2160, 'aspect': 1.78},
'1080p': {'resolution_width': 1920, 'resolution_height': 1080, 'aspect': 1.78},
'1080i': {'resolution_width': 1920, 'resolution_height': 1080, 'aspect': 1.78},
'720p': {'resolution_width': 1280, 'resolution_height': 720, 'aspect': 1.78},

2
couchpotato/core/plugins/score/scores.py

@ -19,7 +19,7 @@ name_scores = [
# Audio
'dts:4', 'ac3:2',
# Quality
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
'720p:10', '1080p:10', '2160p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
# Language / Subs
'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10', 'dutch:-10',
# Release groups

49
couchpotato/core/plugins/test_browser.py

@ -0,0 +1,49 @@
import sys
import os
import logging
import unittest
from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.plugins.browser import FileBrowser
from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/'
class FileBrowserChrootedTest(TestCase):
def setUp(self):
self.b = FileBrowser()
# TODO : remove scrutch:
self.b.soft_chroot = SoftChroot(CHROOT_DIR)
# Logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# To screen
hdlr = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
hdlr.setFormatter(formatter)
#logger.addHandler(hdlr)
def test_soft_chroot_enabled(self):
self.assertTrue( self.b.soft_chroot.enabled)
def test_view__chrooted_path_none(self):
#def view(self, path = '/', show_hidden = True, **kwargs):
r = self.b.view(None)
self.assertEqual(r['home'], '/')
self.assertEqual(r['parent'], '/')
self.assertTrue(r['is_root'])
def test_view__chrooted_path_chroot(self):
#def view(self, path = '/', show_hidden = True, **kwargs):
for path, parent in [('/asdf','/'), (CHROOT_DIR, '/'), ('/mnk/123/t', '/mnk/123/')]:
r = self.b.view(path)
path_strip = path
if (path.endswith(os.path.sep)):
path_strip = path_strip.rstrip(os.path.sep)
self.assertEqual(r['home'], '/')
self.assertEqual(r['parent'], parent)
self.assertFalse(r['is_root'])

140
couchpotato/core/settings.py

@ -7,7 +7,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat
from couchpotato.core.softchroot import SoftChroot
class Settings(object):
@ -77,7 +77,8 @@ class Settings(object):
return self.p
def sections(self):
return self.p.sections()
res = filter( self.isSectionReadable, self.p.sections())
return res
def connectEvents(self):
addEvent('settings.options', self.addOptions)
@ -106,9 +107,21 @@ class Settings(object):
self.save()
def set(self, section, option, value):
if not self.isOptionWritable(section, option):
self.log.warning('set::option "%s.%s" isn\'t writable', (section, option))
return None
if self.isOptionMeta(section, option):
self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option))
return None
return self.p.set(section, option, value)
def get(self, option = '', section = 'core', default = None, type = None):
if self.isOptionMeta(section, option):
self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option))
return None
try:
try: type = self.types[section][option]
@ -123,6 +136,14 @@ class Settings(object):
return default
def delete(self, option = '', section = 'core'):
if not self.isOptionWritable(section, option):
self.log.warning('delete::option "%s.%s" isn\'t writable', (section, option))
return None
if self.isOptionMeta(section, option):
self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option))
return None
self.p.remove_option(section, option)
self.save()
@ -153,11 +174,30 @@ class Settings(object):
def getValues(self):
values = {}
# TODO : There is two commented "continue" blocks (# COMMENTED_SKIPPING). They both are good...
# ... but, they omit output of values of hidden and non-readable options
# Currently, such behaviour could break the Web UI of CP...
# So, currently this two blocks are commented (but they are required to
# provide secure hidding of options.
for section in self.sections():
# COMMENTED_SKIPPING
#if not self.isSectionReadable(section):
# continue
values[section] = {}
for option in self.p.items(section):
(option_name, option_value) = option
#skip meta options:
if self.isOptionMeta(section, option_name):
continue
# COMMENTED_SKIPPING
#if not self.isOptionReadable(section, option_name):
# continue
is_password = False
try: is_password = self.types[section][option_name] == 'password'
except: pass
@ -189,14 +229,52 @@ class Settings(object):
self.types[section][option] = type
def addOptions(self, section_name, options):
# no additional actions (related to ro-rw options) are required here
if not self.options.get(section_name):
self.options[section_name] = options
else:
self.options[section_name] = mergeDicts(self.options[section_name], options)
def getOptions(self):
return self.options
"""Returns dict of UI-readable options
To check, whether the option is readable self.isOptionReadable() is used
"""
res = {}
if isinstance(self.options, dict):
for section_key in self.options.keys():
section = self.options[section_key]
section_name = section.get('name') if 'name' in section else section_key
if self.isSectionReadable(section_name) and isinstance(section, dict):
s = {}
sg = []
for section_field in section:
if section_field.lower() != 'groups':
s[section_field] = section[section_field]
else:
groups = section['groups']
for group in groups:
g = {}
go = []
for group_field in group:
if group_field.lower() != 'options':
g[group_field] = group[group_field]
else:
for option in group[group_field]:
option_name = option.get('name')
if self.isOptionReadable(section_name, option_name):
go.append(option)
option['writable'] = self.isOptionWritable(section_name, option_name)
if len(go)>0:
g['options'] = go
sg.append(g)
if len(sg)>0:
s['groups'] = sg
res[section_key] = s
return res
def view(self, **kwargs):
return {
@ -210,6 +288,11 @@ class Settings(object):
option = kwargs.get('name')
value = kwargs.get('value')
if (section in self.types) and (option in self.types[section]) and (self.types[section][option] == 'directory'):
soft_chroot_dir = self.get('soft_chroot', default = None)
soft_chroot = SoftChroot(soft_chroot_dir)
value = soft_chroot.add(str(value))
# See if a value handler is attached, use that as value
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True)
@ -221,9 +304,56 @@ class Settings(object):
fireEvent('setting.save.%s.*.after' % section, single = True)
return {
'success': True,
'success': True
}
def isSectionReadable(self, section):
meta = 'section_hidden' + self.optionMetaSuffix()
try:
return not self.p.getboolean(section, meta)
except: pass
# by default - every section is readable:
return True
def isOptionReadable(self, section, option):
meta = option + self.optionMetaSuffix()
if self.p.has_option(section, meta):
meta_v = self.p.get(section, meta).lower()
return (meta_v == 'rw') or (meta_v == 'ro')
# by default - all is writable:
return True
def optionReadableCheckAndWarn(self, section, option):
x = self.isOptionReadable(section, option)
if not x:
self.log.warning('Option "%s.%s" isn\'t readable', (section, option))
return x
def isOptionWritable(self, section, option):
meta = option + self.optionMetaSuffix()
if self.p.has_option(section, meta):
return self.p.get(section, meta).lower() == 'rw'
# by default - all is writable:
return True
def optionMetaSuffix(self):
return '_internal_meta'
def isOptionMeta(self, section, option):
""" A helper method for detecting internal-meta options in the ini-file
For a meta options used following names:
* section_hidden_internal_meta = (True | False) - for section visibility
* <OPTION>_internal_meta = (ro|rw|hidden) - for section visibility
"""
suffix = self.optionMetaSuffix()
return option.endswith(suffix)
def getProperty(self, identifier):
from couchpotato import get_db

64
couchpotato/core/softchroot.py

@ -0,0 +1,64 @@
import os
import sys
class SoftChroot:
def __init__(self, chdir):
self.enabled = False
self.chdir = chdir
if None != self.chdir:
self.chdir = self.chdir.strip()
self.chdir = self.chdir.rstrip(os.path.sep) + os.path.sep
self.enabled = True
def is_root_abs(self, abspath):
if not self.enabled:
raise Exception('chroot disabled')
if None == abspath:
return False
path = abspath.rstrip(os.path.sep) + os.path.sep
return self.chdir == path
def is_subdir(self, path):
if not self.enabled:
return True
if None == path:
return False
if not path.endswith(os.path.sep):
path += os.path.sep
return path.startswith(self.chdir)
def add(self, path):
if not self.enabled:
return path
if None == path or len(path)==0:
return self.chdir
if not path.startswith(os.path.sep):
raise ValueError("path must starts with '/'")
return self.chdir[:-1] + path
def cut(self, path):
if not self.enabled:
return path
if None == path or 0==len(path):
raise ValueError('path is empty')
if path == self.chdir.rstrip(os.path.sep):
return '/'
if not path.startswith(self.chdir):
raise ValueError("path must starts with 'chdir'")
l = len(self.chdir)-1
return path[l:]

54
couchpotato/core/test_softchroot.py

@ -0,0 +1,54 @@
import sys
import os
import logging
import unittest
from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/'
class SoftChrootEnabledTest(TestCase):
def setUp(self):
self.b = SoftChroot(CHROOT_DIR)
def test_enabled(self):
self.assertTrue( self.b.enabled)
def test_is_subdir(self):
self.assertFalse( self.b.is_subdir('') )
self.assertFalse( self.b.is_subdir(None) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_subdir(noslash) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR + 'come') )
def test_is_root_abs(self):
self.assertFalse( self.b.is_root_abs('') )
self.assertFalse( self.b.is_root_abs(None) )
self.assertTrue( self.b.is_root_abs(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_root_abs(noslash) )
self.assertFalse( self.b.is_root_abs(CHROOT_DIR + 'come') )
def test_add(self):
with self.assertRaises(ValueError):
self.b.add('no_leading_slash')
self.assertEqual( self.b.add(None), CHROOT_DIR )
self.assertEqual( self.b.add(''), CHROOT_DIR )
self.assertEqual( self.b.add('/asdf'), CHROOT_DIR + 'asdf' )
def test_cut(self):
with self.assertRaises(ValueError): self.b.cut(None)
with self.assertRaises(ValueError): self.b.cut('')
self.assertEqual( self.b.cut(CHROOT_DIR + 'asdf'), '/asdf' )
self.assertEqual( self.b.cut(CHROOT_DIR), '/' )
self.assertEqual( self.b.cut(CHROOT_DIR.rstrip(os.path.sep)), '/' )

13
couchpotato/runner.py

@ -216,6 +216,19 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
log = CPLog(__name__)
log.debug('Started with options %s', options)
# Check soft-chroot dir exists:
try:
soft_chroot = Env.setting('soft_chroot', section = 'core', default = None, type='unicode' )
if (None != soft_chroot):
soft_chroot = soft_chroot.strip()
if (len(soft_chroot)>0) and (not os.path.isdir(soft_chroot)):
log.error('SOFT-CHROOT is defined, but the folder doesn\'t exist')
return
except:
log.error('Unable to check whether SOFT-CHROOT is defined')
return
# Check available space
try:
total_space, available_space = getFreeSpace(data_dir)

54
couchpotato/static/scripts/combined.base.min.js

@ -187,7 +187,6 @@ var CouchPotato = new Class({
}
window.addEvent("resize", self.resize.bind(self));
self.resize();
self.checkCache();
},
checkCache: function() {
window.addEventListener("load", function() {
@ -1336,12 +1335,14 @@ var OptionBase = new Class({
klass: "",
focused_class: "focused",
save_on_change: true,
read_only: false,
initialize: function(section, name, value, options) {
var self = this;
self.setOptions(options);
self.section = section;
self.name = name;
self.value = self.previous_value = value;
self.read_only = !(options && options.writable);
self.createBase();
self.create();
self.createHint();
@ -1354,7 +1355,7 @@ var OptionBase = new Class({
},
createBase: function() {
var self = this;
self.el = new Element("div.ctrlHolder." + self.section + "_" + self.name + (self.klass ? "." + self.klass : ""));
self.el = new Element("div.ctrlHolder." + self.section + "_" + self.name + (self.klass ? "." + self.klass : "") + (self.read_only ? ".read_only" : ""));
},
create: function() {},
createLabel: function() {
@ -1393,7 +1394,11 @@ var OptionBase = new Class({
}
},
save: function() {
var self = this, value = self.getValue();
var self = this, value = self.getValue(), ro = self.read_only;
if (ro) {
console.warn("Unable to save readonly-option " + self.section + "." + self.name);
return;
}
App.fireEvent("setting.save." + self.section + "." + self.name, value);
Api.request("settings.save", {
data: {
@ -1451,7 +1456,9 @@ Option.String = new Class({
type: "text",
name: self.postName(),
value: self.getSettingValue(),
placeholder: self.getPlaceholder()
placeholder: self.getPlaceholder(),
readonly: self.read_only,
disabled: self.read_only
}));
},
getPlaceholder: function() {
@ -1464,7 +1471,9 @@ Option.Dropdown = new Class({
create: function() {
var self = this;
self.el.adopt(self.createLabel(), new Element("div.select_wrapper.icon-dropdown").grab(self.input = new Element("select", {
name: self.postName()
name: self.postName(),
readonly: self.read_only,
disabled: self.read_only
})));
Object.each(self.options.values, function(value) {
new Element("option", {
@ -1486,7 +1495,9 @@ Option.Checkbox = new Class({
name: self.postName(),
type: "checkbox",
checked: self.getSettingValue(),
id: randomId
id: randomId,
readonly: self.read_only,
disabled: self.read_only
}));
},
getValue: function() {
@ -1504,7 +1515,9 @@ Option.Password = new Class({
type: "text",
name: self.postName(),
value: self.getSettingValue() ? "********" : "",
placeholder: self.getPlaceholder()
placeholder: self.getPlaceholder(),
readonly: self.read_only,
disabled: self.read_only
}));
self.input.addEvent("focus", function() {
self.input.set("value", "");
@ -1524,7 +1537,9 @@ Option.Enabler = new Class({
self.el.adopt(new Element("label.switch").adopt(self.input = new Element("input", {
type: "checkbox",
checked: self.getSettingValue(),
id: "r-" + randomString()
id: "r-" + randomString(),
readonly: self.read_only,
disabled: self.read_only
}), new Element("div.toggle")));
},
changed: function() {
@ -1561,12 +1576,23 @@ Option.Directory = new Class({
current_dir: "",
create: function() {
var self = this;
if (self.read_only) {
self.el.adopt(self.createLabel(), self.input = new Element("input", {
type: "text",
name: self.postName(),
value: self.getSettingValue(),
readonly: true,
disabled: true
}));
} else {
self.el.adopt(self.createLabel(), self.directory_inlay = new Element("span.directory", {
events: {
click: self.showBrowser.bind(self)
}
}).adopt(self.input = new Element("input", {
value: self.getSettingValue(),
readonly: self.read_only,
disabled: self.read_only,
events: {
change: self.filterDirectory.bind(self),
keydown: function(e) {
@ -1576,6 +1602,7 @@ Option.Directory = new Class({
paste: self.filterDirectory.bind(self)
}
})));
}
self.cached = {};
},
filterDirectory: function(e) {
@ -1992,14 +2019,16 @@ var AboutSettingTab = new Class({
self.createAbout();
});
self.settings.default_action = "about";
self.hide_dirs = !!App.options && App.options.hide_about_dirs;
},
createAbout: function() {
var self = this;
var millennium = new Date(2008, 7, 16), today = new Date(), one_day = 1e3 * 60 * 60 * 24;
var about_block;
self.settings.createGroup({
label: "About This CouchPotato",
name: "variables"
}).inject(self.content).adopt(new Element("dl.info").adopt(new Element("dt[text=Version]"), self.version_text = new Element("dd.version", {
}).inject(self.content).adopt((about_block = new Element("dl.info")).adopt(new Element("dt[text=Version]"), self.version_text = new Element("dd.version", {
text: "Getting version...",
events: {
click: App.checkForUpdate.bind(App, function(json) {
@ -2014,7 +2043,9 @@ var AboutSettingTab = new Class({
}
}), new Element("dt[text=Updater]"), self.updater_type = new Element("dd.updater"), new Element("dt[text=ID]"), new Element("dd", {
text: App.getOption("pid")
}), new Element("dt[text=Directories]"), new Element("dd", {
})));
if (!self.hide_dirs) {
about_block.adopt(new Element("dt[text=Directories]"), new Element("dd", {
text: App.getOption("app_dir")
}), new Element("dd", {
text: App.getOption("data_dir")
@ -2022,7 +2053,8 @@ var AboutSettingTab = new Class({
html: App.getOption("args")
}), new Element("dd", {
html: App.getOption("options")
})));
}));
}
if (!self.fillVersion(Updater.getInfo())) Updater.addEvent("loaded", self.fillVersion.bind(self));
self.settings.createGroup({
name: "Help Support CouchPotato"

17
couchpotato/static/scripts/page/about.js

@ -28,7 +28,7 @@ var AboutSettingTab = new Class({
});
self.settings.default_action = 'about';
self.hide_dirs = !! App.options && App.options.hide_about_dirs;
},
createAbout: function(){
@ -38,11 +38,13 @@ var AboutSettingTab = new Class({
today = new Date(),
one_day = 1000*60*60*24;
var about_block;
self.settings.createGroup({
'label': 'About This CouchPotato',
'name': 'variables'
}).inject(self.content).adopt(
new Element('dl.info').adopt(
(about_block = new Element('dl.info')).adopt(
new Element('dt[text=Version]'),
self.version_text = new Element('dd.version', {
'text': 'Getting version...',
@ -58,18 +60,25 @@ var AboutSettingTab = new Class({
}
}
}),
new Element('dt[text=Updater]'),
self.updater_type = new Element('dd.updater'),
new Element('dt[text=ID]'),
new Element('dd', {'text': App.getOption('pid')}),
new Element('dd', {'text': App.getOption('pid')})
)
);
if (!self.hide_dirs){
about_block.adopt(
new Element('dt[text=Directories]'),
new Element('dd', {'text': App.getOption('app_dir')}),
new Element('dd', {'text': App.getOption('data_dir')}),
new Element('dt[text=Startup Args]'),
new Element('dd', {'html': App.getOption('args')}),
new Element('dd', {'html': App.getOption('options')})
)
);
}
if(!self.fillVersion(Updater.getInfo()))
Updater.addEvent('loaded', self.fillVersion.bind(self));

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

@ -334,6 +334,7 @@ var OptionBase = new Class({
klass: '',
focused_class: 'focused',
save_on_change: true,
read_only: false,
initialize: function(section, name, value, options){
var self = this;
@ -342,6 +343,7 @@ var OptionBase = new Class({
self.section = section;
self.name = name;
self.value = self.previous_value = value;
self.read_only = !(options && options.writable);
self.createBase();
self.create();
@ -363,7 +365,11 @@ var OptionBase = new Class({
*/
createBase: function(){
var self = this;
self.el = new Element('div.ctrlHolder.' + self.section + '_' + self.name + (self.klass ? '.' + self.klass : ''));
self.el = new Element('div.ctrlHolder.' +
self.section + '_' + self.name +
(self.klass ? '.' + self.klass : '') +
(self.read_only ? '.read_only' : '')
);
},
create: function(){
@ -418,7 +424,13 @@ var OptionBase = new Class({
save: function(){
var self = this,
value = self.getValue();
value = self.getValue(),
ro = self.read_only;
if (ro) {
console.warn('Unable to save readonly-option ' + self.section + '.' + self.name);
return;
}
App.fireEvent('setting.save.'+self.section+'.'+self.name, value);
@ -493,7 +505,9 @@ Option.String = new Class({
'type': 'text',
'name': self.postName(),
'value': self.getSettingValue(),
'placeholder': self.getPlaceholder()
'placeholder': self.getPlaceholder(),
'readonly' : self.read_only,
'disabled' : self.read_only
})
);
},
@ -513,7 +527,9 @@ Option.Dropdown = new Class({
self.createLabel(),
new Element('div.select_wrapper.icon-dropdown').grab(
self.input = new Element('select', {
'name': self.postName()
'name': self.postName(),
'readonly' : self.read_only,
'disabled' : self.read_only
})
)
);
@ -545,7 +561,9 @@ Option.Checkbox = new Class({
'name': self.postName(),
'type': 'checkbox',
'checked': self.getSettingValue(),
'id': randomId
'id': randomId,
'readonly' : self.read_only,
'disabled' : self.read_only
})
);
@ -570,7 +588,9 @@ Option.Password = new Class({
'type': 'text',
'name': self.postName(),
'value': self.getSettingValue() ? '********' : '',
'placeholder': self.getPlaceholder()
'placeholder': self.getPlaceholder(),
'readonly' : self.read_only,
'disabled' : self.read_only
})
);
@ -597,7 +617,9 @@ Option.Enabler = new Class({
self.input = new Element('input', {
'type': 'checkbox',
'checked': self.getSettingValue(),
'id': 'r-'+randomString()
'id': 'r-'+randomString(),
'readonly' : self.read_only,
'disabled' : self.read_only,
}),
new Element('div.toggle')
)
@ -652,7 +674,19 @@ Option.Directory = new Class({
create: function(){
var self = this;
if (self.read_only) {
// create disabled textbox:
self.el.adopt(
self.createLabel(),
self.input = new Element('input', {
'type': 'text',
'name': self.postName(),
'value': self.getSettingValue(),
'readonly' : true,
'disabled' : true
})
);
} else {
self.el.adopt(
self.createLabel(),
self.directory_inlay = new Element('span.directory', {
@ -662,6 +696,8 @@ Option.Directory = new Class({
}).adopt(
self.input = new Element('input', {
'value': self.getSettingValue(),
'readonly' : self.read_only,
'disabled' : self.read_only,
'events': {
'change': self.filterDirectory.bind(self),
'keydown': function(e){
@ -674,6 +710,7 @@ Option.Directory = new Class({
})
)
);
}
self.cached = {};
},

70
couchpotato/static/style/combined.min.css

@ -41,10 +41,9 @@
.search_form .results_container .results .media_result{font-size:12px}
.search_form .results_container .results .media_result .options{left:0}
.search_form .results_container .results .media_result .options>div{padding:3px}
.search_form .results_container .results .media_result .options select{min-width:0;margin-right:2px}
}
.search_form .results_container .results .media_result .options select{display:block;height:100%;width:100%}
@media (max-width:480px){.search_form .results_container .results .media_result .options select{min-width:0;margin-right:2px}
}
.search_form .results_container .results .media_result .options .title{margin-right:5px;width:210px}
@media (max-width:480px){.search_form .results_container .results .media_result .options .title{width:140px;margin-right:2px}
}
@ -98,8 +97,7 @@
}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results{max-height:400px}
.page.home .search_form .wrapper .results_container .results .media_result{height:66px}
}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results .media_result .thumbnail{width:40px}
.page.home .search_form .wrapper .results_container .results .media_result .thumbnail{width:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options{left:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{margin-right:5px;width:320px}
}
@ -114,7 +112,7 @@
.big_search{background:#ebebeb}
.dark .big_search{background:#353535}
.page.movies{bottom:auto;z-index:21;height:80px}
.page.movies_manage,.page.movies_wanted{top:80px;padding:0;will-change:top;transition:top 300ms cubic-bezier(.9,0,.1,1)}
.page.movies_manage,.page.movies_wanted{top:80px;padding:0;will-change:top;transition:top .3s cubic-bezier(.9,0,.1,1)}
@media (max-width:480px){.page.movies{height:44px}
.page.movies_manage,.page.movies_wanted{top:44px}
}
@ -123,7 +121,7 @@
.page.movies_manage .empty_manage,.page.movies_wanted .empty_manage{padding:20px}
.page.movies_manage .empty_manage .after_manage,.page.movies_wanted .empty_manage .after_manage{margin-top:20px}
.movie .ripple,.movie input[type=checkbox]{display:none}
.with_navigation .movie input[type=checkbox]{display:inline-block;position:absolute;will-change:opacity;transition:opacity 200ms;opacity:0;z-index:2;cursor:pointer}
.with_navigation .movie input[type=checkbox]{display:inline-block;position:absolute;will-change:opacity;transition:opacity .2s;opacity:0;z-index:2;cursor:pointer}
@media (max-width:480px){.with_navigation .movie input[type=checkbox]{display:none}
}
.with_navigation .movie input[type=checkbox]:hover{opacity:1!important}
@ -145,6 +143,7 @@
.movies{position:relative}
.movies .no_movies{display:block;padding:20px}
@media (max-width:768px){.movies .no_movies{padding:10px}
.movies>.description{display:none}
}
.movies .no_movies a{color:#ac0000}
.dark .movies .no_movies a{color:#f85c22}
@ -153,10 +152,9 @@
.dark .movies .message a{color:#f85c22}
.movies.movies>h2{padding:0 20px;line-height:80px}
@media (max-width:480px){.movies.movies>h2{line-height:44px;padding:0 10px}
.movies .movie .actions{pointer-events:none}
}
.movies>.description{position:absolute;top:0;right:20px;width:auto;line-height:80px;opacity:.7}
@media (max-width:768px){.movies>.description{display:none}
}
.movies>.description a{color:#ac0000;display:inline}
.dark .movies>.description a{color:#f85c22}
.movies>.loading{background:#FFF}
@ -179,7 +177,7 @@
.list_list .movie .poster{display:none}
.list_list .movie .info{padding:10px 20px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center}
.list_list .movie .info .title{-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto}
.list_list .movie .info .title span{transition:margin 200ms cubic-bezier(.9,0,.1,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.list_list .movie .info .title span{transition:margin .2s cubic-bezier(.9,0,.1,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media (max-width:768px){.movies .progress div{width:100%}
.list_list .movie .info{display:block;padding:10px}
.list_list .movie .info .title{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap}
@ -187,8 +185,7 @@
}
.list_list .movie .info .title .year{display:inline-block;margin:0 10px;opacity:.5}
.list_list .movie .info .eta{font-size:.8em;opacity:.5;margin-right:4px}
@media (max-width:480px){.movies .movie .actions{pointer-events:none}
.list_list .movie .info .eta{display:none}
@media (max-width:480px){.list_list .movie .info .eta{display:none}
}
.list_list .movie .info .quality{clear:both;overflow:hidden}
.list_list .movie .info .quality span{float:left;font-size:.7em;margin:2px 0 0 2px}
@ -266,7 +263,7 @@
.thumb_list .movie .info .eta{opacity:.5;float:right;margin-left:4px}
.thumb_list .movie .info .quality{white-space:nowrap;overflow:hidden;font-size:.9em}
.thumb_list .movie .info .quality span{font-size:.8em;margin-right:2px}
.thumb_list .movie .actions{background-image:linear-gradient(25deg,rgba(172,0,0,.3) 0,#ac0000 80%);will-change:opacity,visibility;transition:all 400ms;transition-property:opacity,visibility;opacity:0;visibility:hidden;position:absolute;top:0;right:0;bottom:0;left:0;text-align:right}
.thumb_list .movie .actions{background-image:linear-gradient(25deg,rgba(172,0,0,.3) 0,#ac0000 80%);will-change:opacity,visibility;transition:all .4s;transition-property:opacity,visibility;opacity:0;visibility:hidden;position:absolute;top:0;right:0;bottom:0;left:0;text-align:right}
.dark .thumb_list .movie .actions{background-image:linear-gradient(25deg,rgba(248,92,34,.3) 0,#f85c22 80%)}
.thumb_list .movie .actions .action{position:relative;margin-right:10px;float:right;clear:both}
.thumb_list .movie .actions .action:first-child{margin-top:10px}
@ -278,11 +275,11 @@
@media (max-width:480px){.thumb_list .movie:hover .actions{display:none}
.page.movie_details{left:0}
}
.page.movie_details .overlay{position:fixed;top:0;bottom:0;right:0;left:132px;background:rgba(0,0,0,.6);border-radius:3px 0 0 3px;opacity:0;will-change:opacity;-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg);transition:opacity 300ms ease 400ms;z-index:1}
.page.movie_details .overlay{position:fixed;top:0;bottom:0;right:0;left:132px;background:rgba(0,0,0,.6);border-radius:3px 0 0 3px;opacity:0;will-change:opacity;-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg);transition:opacity .3s ease .4s;z-index:1}
.page.movie_details .overlay .ripple{background:#FFF}
@media (max-width:480px){.page.movie_details .overlay{left:0;border-radius:0;transition:none}
}
.page.movie_details .overlay .close{display:inline-block;text-align:center;font-size:60px;line-height:80px;color:#FFF;width:100%;height:100%;opacity:0;will-change:opacity;transition:opacity 300ms ease 200ms}
.page.movie_details .overlay .close{display:inline-block;text-align:center;font-size:60px;line-height:80px;color:#FFF;width:100%;height:100%;opacity:0;will-change:opacity;transition:opacity .3s ease .2s}
.page.movie_details .overlay .close:before{display:block;width:44px}
.page.movie_details .scroll_content{position:fixed;z-index:2;top:0;bottom:0;right:0;left:176px;background:#FFF;border-radius:3px 0 0 3px;overflow-y:auto;will-change:transform;-webkit-transform:translateX(100%) rotateZ(360deg);transform:translateX(100%) rotateZ(360deg);transition:-webkit-transform 450ms cubic-bezier(.9,0,.1,1);transition:transform 450ms cubic-bezier(.9,0,.1,1)}
.dark .page.movie_details .scroll_content{background:#2d2d2d}
@ -293,6 +290,7 @@
.page.movie_details .scroll_content>.head{padding:0;line-height:44px}
.page.movie_details .scroll_content>.head h1{min-width:100%;line-height:44px}
.page.movie_details .scroll_content>.head h1 .more_menu{width:100%}
.page.movie_details .scroll_content>.head h1 .more_menu .icon-dropdown:before{right:10px}
}
.page.movie_details .scroll_content>.head h1 .more_menu a{color:#000}
.dark .page.movie_details .scroll_content>.head h1 .more_menu a{color:#FFF}
@ -306,8 +304,7 @@
.page.movie_details .scroll_content>.head .more_menu .icon-dropdown:before{position:absolute;right:10px;top:-2px;opacity:.2}
.page.movie_details .scroll_content>.head .more_menu .icon-dropdown:hover:before{opacity:1}
.page.movie_details .scroll_content>.head .more_menu .wrapper{top:70px;padding-top:4px;border-radius:3px 3px 0 0;font-size:14px}
@media (max-width:480px){.page.movie_details .scroll_content>.head h1 .more_menu .icon-dropdown:before{right:10px}
.page.movie_details .scroll_content>.head .more_menu>a{line-height:44px}
@media (max-width:480px){.page.movie_details .scroll_content>.head .more_menu>a{line-height:44px}
.page.movie_details .scroll_content>.head .more_menu .wrapper{top:25px}
}
.page.movie_details .scroll_content>.head .more_menu .wrapper:before{top:0;left:auto;right:22px}
@ -334,7 +331,7 @@
.page.movie_details .files span,.page.movie_details .releases .item span{white-space:nowrap;padding:6.67px 0;overflow:hidden;text-overflow:ellipsis}
.page.movie_details.show{pointer-events:auto}
.page.movie_details.show .overlay{opacity:1;transition-delay:0s}
.page.movie_details.show .overlay .close{opacity:1;transition-delay:300ms}
.page.movie_details.show .overlay .close{opacity:1;transition-delay:.3s}
.page.movie_details.show .scroll_content{transition-delay:50ms;-webkit-transform:translateX(0) rotateZ(360deg);transform:translateX(0) rotateZ(360deg)}
.page.movie_details .section_description .meta{text-align:right;font-style:italic;font-size:.9em}
.page.movie_details .section_description .meta span{display:inline-block;margin:10px 10px 0}
@ -372,16 +369,17 @@
.page.movie_details .releases .item .name{width:100%;font-weight:700}
.page.movie_details .releases .item.head{display:none}
.page.movie_details .releases .item .actions{width:100%;text-align:center}
.page.movie_details .releases .item .actions a{text-align:center}
}
.page.movie_details .releases .item .actions a{display:inline-block;vertical-align:top;padding:6.67px;min-width:26px;color:#000}
.dark .page.movie_details .releases .item .actions a{color:#FFF}
.page.movie_details .releases .item .actions a:hover{color:#ac0000}
.dark .page.movie_details .releases .item .actions a:hover{color:#f85c22}
.page.movie_details .releases .item .actions a:after{margin-left:3px;font-size:.9em}
@media (max-width:480px){.page.movie_details .releases .item .actions a{text-align:center}
.page.movie_details .releases .item .actions a.icon-info:after{content:"more info"}
@media (max-width:480px){.page.movie_details .releases .item .actions a.icon-info:after{content:"more info"}
.page.movie_details .releases .item .actions a.icon-download:after{content:"download"}
.page.movie_details .releases .item .actions a.icon-cancel:after{content:"ignore"}
.page.movie_details .section_trailer.section_trailer{max-height:450px}
}
.page.movie_details .releases .status{min-width:70px;max-width:70px}
.page.movie_details .releases .status:before{content:"Status:"}
@ -400,16 +398,15 @@
.dark .page.movie_details .section_trailer.section_trailer{background:#111}
.page.movie_details .section_trailer.section_trailer.no_trailer{display:none}
.page.movie_details .section_trailer.section_trailer .trailer_container{max-height:450px;position:relative;overflow:hidden;max-width:800px;margin:0 auto;cursor:pointer}
.page.movie_details .section_trailer.section_trailer .trailer_container .background{opacity:0;background:center no-repeat;background-size:cover;position:relative;z-index:1;max-height:450px;padding-bottom:56.25%;will-change:opacity;transition:opacity 1000ms}
.page.movie_details .section_trailer.section_trailer .trailer_container .background{opacity:0;background:center no-repeat;background-size:cover;position:relative;z-index:1;max-height:450px;padding-bottom:56.25%;will-change:opacity;transition:opacity 1s}
.page.movie_details .section_trailer.section_trailer .trailer_container .background.visible{opacity:.4}
.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play{opacity:.9;position:absolute;z-index:2;text-align:center;width:100%;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);will-change:opacity;transition:all 300ms;color:#FFF;font-size:110px}
.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play{opacity:.9;position:absolute;z-index:2;text-align:center;width:100%;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);will-change:opacity;transition:all .3s;color:#FFF;font-size:110px}
@media (max-width:1024px){.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play{font-size:55px}
}
@media (max-width:480px){.page.movie_details .section_trailer.section_trailer{max-height:450px}
.page.movie_details .section_trailer.section_trailer .trailer_container{margin-bottom:10px}
@media (max-width:480px){.page.movie_details .section_trailer.section_trailer .trailer_container{margin-bottom:10px}
.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play{font-size:31.43px}
}
.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play span{transition:all 300ms;opacity:.9;position:absolute;font-size:1em;top:50%;left:50%;margin-left:55px;-webkit-transform:translateY(-54%);transform:translateY(-54%);will-change:opacity}
.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play span{transition:all .3s;opacity:.9;position:absolute;font-size:1em;top:50%;left:50%;margin-left:55px;-webkit-transform:translateY(-54%);transform:translateY(-54%);will-change:opacity}
@media (max-width:1024px){.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play span{margin-left:27.5px}
}
@media (max-width:480px){.page.movie_details .section_trailer.section_trailer .trailer_container .icon-play span{margin-left:15.71px}
@ -424,7 +421,7 @@
.page.movie_details .section_trailer.section_trailer .trailer_container:hover .icon-play,.page.movie_details .section_trailer.section_trailer .trailer_container:hover .icon-play span{opacity:1}
.page.movie_details .section_trailer.section_trailer .trailer_container iframe{position:absolute;width:100%;height:100%;border:0;top:0;left:0;max-height:450px;z-index:10}
.alph_nav{position:relative}
.alph_nav .mass_edit_form{display:-webkit-flex;display:-ms-flexbox;display:flex;background:#FFF;position:fixed;top:80px;right:0;left:132px;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center;will-change:max-height;transition:max-height 300ms cubic-bezier(.9,0,.1,1);max-height:0;overflow:hidden}
.alph_nav .mass_edit_form{display:-webkit-flex;display:-ms-flexbox;display:flex;background:#FFF;position:fixed;top:80px;right:0;left:132px;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center;will-change:max-height;transition:max-height .3s cubic-bezier(.9,0,.1,1);max-height:0;overflow:hidden}
.dark .alph_nav .mass_edit_form{background:#2d2d2d}
.mass_editing .alph_nav .mass_edit_form{max-height:44px}
.alph_nav .mass_edit_form>*{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}
@ -439,17 +436,16 @@
.alph_nav .menus .actions>a:hover,.alph_nav .menus .counter>a:hover,.alph_nav .menus .more_menu>a:hover{background:#ebebeb}
.dark .alph_nav .menus .actions>a:hover,.dark .alph_nav .menus .counter>a:hover,.dark .alph_nav .menus .more_menu>a:hover{background:#353535}
@media (max-width:768px){.alph_nav .menus .actions>a,.alph_nav .menus .counter>a,.alph_nav .menus .more_menu>a{line-height:44px}
.alph_nav .menus .counter{display:none}
}
.alph_nav .menus .counter{line-height:80px;padding:0 10px}
@media (max-width:768px){.alph_nav .menus .counter{display:none}
}
.alph_nav .menus .actions a{display:inline-block}
.alph_nav .menus .actions .active{display:none}
.alph_nav .menus .filter .wrapper{width:320px}
.alph_nav .menus .filter .button{margin-top:-2px}
.alph_nav .menus .filter .search{position:relative}
.alph_nav .menus .filter .search:before{position:absolute;height:100%;line-height:38px;padding-left:10px;font-size:16px;opacity:.5}
.alph_nav .menus .filter .search input{width:100%;padding:10px 10px 10px 30px;background:#FFF;border:none #ebebeb;border-bottom:1px solid transparent}
.alph_nav .menus .filter .search input{width:100%;padding:10px 10px 10px 30px;background:#FFF;border:#ebebeb;border-bottom:1px solid transparent}
.dark .alph_nav .menus .filter .search input{background:#2d2d2d;border-color:#353535}
.alph_nav .menus .filter .numbers{padding:10px}
.alph_nav .menus .filter .numbers li{float:left;width:10%;height:30px;line-height:30px;text-align:center;opacity:.2;cursor:default;border:0}
@ -693,18 +689,18 @@ input[type=text],textarea{-webkit-appearance:none}
@media (max-width:480px){.header .navigation .logo{font-size:28px;line-height:44px;height:44px}
.header .navigation .logo:after{content:'CP'}
.header .navigation .logo span{display:none}
.header .navigation ul li{line-height:0}
}
.header .navigation ul{padding:0;margin:0}
.header .navigation ul li{display:block}
.header .navigation ul li a{padding:10px 20px;display:block;position:relative}
.header .navigation ul li a:before{position:absolute;width:100%;display:none;text-align:center;font-size:18px;text-indent:0}
@media (max-width:480px){.header .navigation ul li{line-height:0}
.header .navigation ul li a{line-height:24px;height:44px;padding:10px 0;text-align:center}
@media (max-width:480px){.header .navigation ul li a{line-height:24px;height:44px;padding:10px 0;text-align:center}
.header .navigation ul li a span{display:none}
.header .navigation ul li a:before{display:block}
}
.header .navigation ul li a.icon-home:before{font-size:24px}
.header .donate{position:absolute;bottom:44px;left:0;right:0;padding:10px 20px;transition:background 200ms}
.header .donate{position:absolute;bottom:44px;left:0;right:0;padding:10px 20px;transition:background .2s}
.header .donate:before{display:none;font-size:20px;text-align:center}
@media (max-width:480px){.header .donate{bottom:132px;padding:10px 0}
.header .donate span{display:none}
@ -811,7 +807,7 @@ input[type=text],textarea{-webkit-appearance:none}
.question a{border-color:#FFF;color:#FFF;transition:none}
.question a:hover{background:#ac0000;color:#FFF}
.dark .question a:hover{background:#f85c22}
.mask{background:rgba(0,0,0,.8);z-index:1000;text-align:center;bottom:0;left:0;opacity:0;transition:opacity 500ms}
.mask{background:rgba(0,0,0,.8);z-index:1000;text-align:center;bottom:0;left:0;opacity:0;transition:opacity .5s}
.mask .message,.mask .spinner{position:absolute;top:50%;left:50%}
.mask .message{color:#FFF;text-align:center;width:320px;margin:-49px 0 0 -160px;font-size:16px}
.mask .message h1{font-size:1.5em}
@ -884,6 +880,7 @@ input[type=text],textarea{-webkit-appearance:none}
@media (max-width:480px){.page.settings fieldset .ctrlHolder{-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;padding:6.67px 0 6.67px 10px}
.page.settings fieldset .ctrlHolder input,.page.settings fieldset .ctrlHolder label,.page.settings fieldset .ctrlHolder select,.page.settings fieldset .ctrlHolder textarea{-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto}
.page.settings fieldset .ctrlHolder input[type=checkbox]{margin-right:20px;-webkit-flex:none;-ms-flex:none;flex:none}
.page.settings fieldset .ctrlHolder .select_wrapper{width:100%}
}
.page.settings fieldset .ctrlHolder .select_wrapper{position:relative;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}
.page.settings fieldset .ctrlHolder .select_wrapper select{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;min-width:200px;border-radius:0}
@ -892,11 +889,13 @@ input[type=text],textarea{-webkit-appearance:none}
.page.settings fieldset .ctrlHolder .formHint{-webkit-flex:1;-ms-flex:1;flex:1;opacity:.8;margin-left:20px}
.page.settings fieldset .ctrlHolder .formHint a{font-weight:400;color:#ac0000;text-decoration:underline}
.dark .page.settings fieldset .ctrlHolder .formHint a{color:#f85c22}
@media (max-width:480px){.page.settings fieldset .ctrlHolder .select_wrapper{width:100%}
.page.settings fieldset .ctrlHolder .formHint{min-width:100%;margin-left:0}
@media (max-width:480px){.page.settings fieldset .ctrlHolder .formHint{min-width:100%;margin-left:0}
}
.page.settings fieldset .ctrlHolder.test_button a{margin:0}
.page.settings fieldset .ctrlHolder.test_button .success{margin-left:10px}
.page.settings fieldset .ctrlHolder.read_only{opacity:.5}
.page.settings fieldset .ctrlHolder.read_only label{position:relative}
.page.settings fieldset .ctrlHolder.read_only label:after{left:0;bottom:-10px;position:absolute;content:'(read-only)';font-size:.7em}
.page.settings fieldset.disabled .ctrlHolder{display:none}
.page.settings fieldset.disabled>.ctrlHolder:first-child{display:-webkit-flex;display:-ms-flexbox;display:flex}
.page.settings fieldset.enabler{display:block}
@ -970,12 +969,11 @@ input[type=text],textarea{-webkit-appearance:none}
}
.page.settings .tab_about .info dd{float:right;width:80%;padding:0;margin:0;font-style:italic}
@media (max-width:480px){.page.settings .tab_about .info dd{float:none;width:auto;margin-bottom:10px}
.page.settings .directory{width:100%}
}
.page.settings .tab_about .info dd.version{cursor:pointer}
.page.settings .tab_about .group_actions>div{padding:20px;text-align:center}
.page.settings .tab_about .group_actions a{margin:0 10px;font-size:20px}
@media (max-width:480px){.page.settings .directory{width:100%}
}
.page.settings .directory input{width:100%}
.page.settings .multi_directory .delete{color:#ac0000;padding:0 10px;opacity:.6;font-size:1.5em}
.dark .page.settings .multi_directory .delete{color:#f85c22}

16
couchpotato/static/style/settings.scss

@ -275,6 +275,22 @@
margin-left: $padding / 2;
}
}
&.read_only {
opacity: .5;
label {
position: relative;
}
label:after {
left: 0;
bottom: -10px;
position: absolute;
content: '(read-only)';
font-size: .7em;
}
}
}
&.disabled {

3
couchpotato/templates/index.html

@ -85,7 +85,8 @@
'app_dir': {{ json_encode(Env.get('app_dir', unicode = True)) }},
'data_dir': {{ json_encode(Env.get('data_dir', unicode = True)) }},
'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)) }},
'hide_about_dirs' : {{ json_encode( Env.setting('hide_about_dirs', default=False, section = 'core', type='bool') ) }}
});
})

2
do.tests.sh

@ -0,0 +1,2 @@
#!/bin/sh
PYTHONPATH=./libs python -m unittest discover -v -s ./couchpotato/

8
libs/unrar2/__init__.py

@ -33,7 +33,7 @@ similar to the C interface provided by UnRAR. There is also a
higher level interface which makes some common operations easier.
"""
__version__ = '0.99.3'
__version__ = '0.99.6'
try:
WindowsError
@ -159,6 +159,12 @@ class RarFile(RarFileImplementation):
checker = condition2checker(condition)
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)
def get_volume(self):
"""Determine which volume is it in a multi-volume archive. Returns None if it's not a
multi-volume archive, 0-based volume number otherwise."""
return RarFileImplementation.get_volume(self)
def condition2checker(condition):
"""Converts different condition types to callback"""
if type(condition) in [str, unicode]:

57
libs/unrar2/unix.py

@ -23,14 +23,16 @@
# Unix version uses unrar command line executable
import platform
import stat
import subprocess
import gc
import os
import os.path
import time
import re
import os, os.path
import time, re
from rar_exceptions import *
from dateutil.parser import parse
from rar_exceptions import *
class UnpackerNotInstalled(Exception): pass
@ -86,13 +88,13 @@ class RarFileImplementation(object):
accum = []
source = iter(stdoutdata.splitlines())
line = ''
while not (line.startswith('UNRAR')):
while (line.find('RAR ') == -1):
line = source.next()
signature = line
# The code below is mighty flaky
# and will probably crash on localized versions of RAR
# but I see no safe way to rewrite it using a CLI tool
if signature.startswith("UNRAR 4"):
if signature.find("RAR 4") > -1:
rar_executable_version = 4
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
if line.strip().endswith('is not RAR archive'):
@ -106,7 +108,7 @@ class RarFileImplementation(object):
self.comment = '\n'.join(accum[:-1])
else:
self.comment = None
elif signature.startswith("UNRAR 5"):
elif signature.find("RAR 5") > -1:
rar_executable_version = 5
line = source.next()
while not line.startswith('Archive:'):
@ -167,6 +169,7 @@ class RarFileImplementation(object):
data['isdir'] = 'd' in attr.lower()
data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M')
data['comment'] = None
data['volume'] = None
yield data
accum = []
i += 1
@ -180,8 +183,9 @@ class RarFileImplementation(object):
data['size'] = int(fields[1])
attr = fields[0]
data['isdir'] = 'd' in attr.lower()
data['datetime'] = time.strptime(fields[2] + " " + fields[3], '%d-%m-%y %H:%M')
data['datetime'] = parse(fields[2] + " " + fields[3]).timetuple()
data['comment'] = None
data['volume'] = None
yield data
i += 1
line = source.next()
@ -227,4 +231,41 @@ class RarFileImplementation(object):
def destruct(self):
pass
def get_volume(self):
command = "v" if rar_executable_version == 4 else "l"
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"):
raise FileOpenError
source = iter(stdoutdata.splitlines())
line = ''
while not line.startswith('-----------'):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
if line.startswith("CRC failed") or line.startswith("Checksum error"):
raise IncorrectRARPassword
line = source.next()
line = source.next()
if rar_executable_version == 4:
while not line.startswith('-----------'):
line = source.next()
line = source.next()
items = line.strip().split()
if len(items)>4 and items[4]=="volume":
return int(items[5]) - 1
else:
return None
elif rar_executable_version == 5:
while not line.startswith('-----------'):
line = source.next()
line = source.next()
items = line.strip().split()
if items[1]=="volume":
return int(items[2]) - 1
else:
return None

39
libs/unrar2/windows.py

@ -25,8 +25,9 @@
from __future__ import generators
from couchpotato.environment import Env
from shutil import copyfile
import ctypes.wintypes
import os.path
import ctypes, ctypes.wintypes
import os, os.path, re
import time
from rar_exceptions import *
@ -43,6 +44,7 @@ ERAR_EREAD = 18
ERAR_EWRITE = 19
ERAR_SMALL_BUF = 20
ERAR_UNKNOWN = 21
ERAR_MISSING_PASSWORD = 22
RAR_OM_LIST = 0
RAR_OM_EXTRACT = 1
@ -75,8 +77,12 @@ if os.path.isfile(dll_copy):
copyfile(dll_file, dll_copy)
unrar = ctypes.WinDLL(dll_copy)
volume_naming1 = re.compile("\.r([0-9]{2})$")
volume_naming2 = re.compile("\.([0-9]{3}).rar$")
volume_naming3 = re.compile("\.part([0-9]+).rar$")
unrar = ctypes.WinDLL(dll_copy)
class RAROpenArchiveDataEx(ctypes.Structure):
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
@ -193,7 +199,7 @@ class RarInfoIterator(object):
self.index = 0
self.headerData = RARHeaderDataEx()
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
if self.res==ERAR_BAD_DATA:
if self.res in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword
self.arc.lockStatus = "locked"
self.arc.needskip = False
@ -213,7 +219,7 @@ class RarInfoIterator(object):
data = {}
data['index'] = self.index
data['filename'] = self.headerData.FileName
data['filename'] = self.headerData.FileNameW
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
@ -257,6 +263,7 @@ class RarFileImplementation(object):
self.lockStatus = "ready"
self.isVolume = archiveData.Flags & 1
def destruct(self):
@ -282,7 +289,7 @@ class RarFileImplementation(object):
c_callback = UNRARCALLBACK(reader._callback)
RARSetCallback(self._handle, c_callback, 1)
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
if tmpres==ERAR_BAD_DATA:
if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword
self.needskip = False
res.append((info, reader.get_result()))
@ -304,11 +311,29 @@ class RarFileImplementation(object):
target = checkres
if overwrite or (not os.path.exists(target)):
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
if tmpres==ERAR_BAD_DATA:
if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword
self.needskip = False
res.append(info)
return res
def get_volume(self):
if not self.isVolume:
return None
headerData = RARHeaderDataEx()
res = RARReadHeaderEx(self._handle, ctypes.byref(headerData))
arcName = headerData.ArcNameW
match3 = volume_naming3.search(arcName)
if match3 != None:
return int(match3.group(1)) - 1
match2 = volume_naming3.search(arcName)
if match2 != None:
return int(match2.group(1))
match1 = volume_naming1.search(arcName)
if match1 != None:
return int(match1.group(1)) + 1
return 0

8
package.json

@ -11,12 +11,12 @@
"devDependencies": {
"grunt": "~0.4.5",
"grunt-autoprefixer": "^3.0.3",
"grunt-concurrent": "~2.0.1",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-cssmin": "~0.13.0",
"grunt-concurrent": "~2.1.0",
"grunt-contrib-clean": "^0.7.0",
"grunt-contrib-cssmin": "~0.14.0",
"grunt-contrib-jshint": "~0.11.2",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-uglify": "~0.9.1",
"grunt-contrib-uglify": "~0.11.0",
"grunt-contrib-watch": "~0.6.1",
"grunt-shell-spawn": "^0.3.8",
"jit-grunt": "^0.9.1",

Loading…
Cancel
Save