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. 79
      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') detail_url = self.getTextElement(nzb, 'guid')
nzb_id = detail_url.split('/')[-1:].pop() 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: if '://' not in detail_url:
detail_url = (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id) 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: if not name:
continue continue
@ -106,7 +114,7 @@ class Base(NZBProvider, RSS):
'name_extra': name_extra, 'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, '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, 'detail_url': detail_url,
'content': self.getTextElement(nzb, 'description'), 'content': self.getTextElement(nzb, 'description'),
'description': 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>, \ '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://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, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=',
'options': [ 'options': [
@ -236,30 +244,30 @@ config = [{
}, },
{ {
'name': 'use', 'name': 'use',
'default': '0,0,0,0,0' 'default': '0,0,0,0,0,0'
}, },
{ {
'name': 'host', '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', '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', 'default': '0,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',

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

@ -55,7 +55,7 @@ class Base(TorrentProvider):
link = cells[1].find('a', attrs = {'class': 'index'}) link = cells[1].find('a', attrs = {'class': 'index'})
full_id = link['href'].replace('details.php?id=', '') 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() name = toUnicode(link.get('title', link.contents[0]).encode('ISO-8859-1')).strip()
results.append({ 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.encoding import sp, ss, toUnicode
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
from couchpotato.core.softchroot import SoftChroot
log = CPLog(__name__) log = CPLog(__name__)
@ -33,6 +33,9 @@ autoload = 'FileBrowser'
class FileBrowser(Plugin): class FileBrowser(Plugin):
def __init__(self): 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 = { addApiView('directory.list', self.view, docs = {
'desc': 'Return the directory list of a given directory', 'desc': 'Return the directory list of a given directory',
'params': { 'params': {
@ -78,11 +81,17 @@ class FileBrowser(Plugin):
return driveletters return driveletters
def view(self, path = '/', show_hidden = True, **kwargs): def view(self, path = '/', show_hidden = True, **kwargs):
home = getUserDir() home = getUserDir()
if self.soft_chroot.enabled:
if not self.soft_chroot.is_subdir(home):
home = self.soft_chroot.chdir
if not path: if not path:
path = home 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: try:
dirs = self.getDirectories(path = path, show_hidden = show_hidden) 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())) log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc()))
dirs = [] dirs = []
if self.soft_chroot.enabled:
dirs = map(self.soft_chroot.cut, dirs)
parent = os.path.dirname(path.rstrip(os.path.sep)) parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep): if parent == path.rstrip(os.path.sep):
parent = '/' parent = '/'
elif parent != '/' and parent[-2:] != ':\\': elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep 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 { return {
'is_root': path == '/', 'is_root': is_root,
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'parent': parent, 'parent': parent,
'home': home + os.path.sep, 'home': home,
'platform': os.name, 'platform': os.name,
'dirs': dirs, 'dirs': dirs,
} }

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

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

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

@ -23,6 +23,7 @@ class QualityPlugin(Plugin):
} }
qualities = [ 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': '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': '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']}, {'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.initialize', self.fill, priority = 10)
addEvent('app.load', self.fillBlank, priority = 120)
addEvent('app.test', self.doTest) addEvent('app.test', self.doTest)
@ -146,7 +148,18 @@ class QualityPlugin(Plugin):
'success': False '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: try:
db = get_db() db = get_db()
@ -156,7 +169,7 @@ class QualityPlugin(Plugin):
existing = None existing = None
try: try:
existing = db.get('quality', q.get('identifier')) existing = db.get('quality', q.get('identifier'), with_doc = reorder)
except RecordNotFound: except RecordNotFound:
pass pass
@ -179,6 +192,10 @@ class QualityPlugin(Plugin):
'finish': [True], 'finish': [True],
'wait_for': [0], 'wait_for': [0],
}) })
elif reorder:
log.info2('Updating quality order')
existing['doc']['order'] = order
db.update(existing['doc'])
order += 1 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.HDSCR.4PARTS.MP4.AAC.ReleaseGroup': {'size': 2401, 'quality': 'scr'},
'Movie.Name.2014.720p.BluRay.x264-ReleaseGroup': {'size': 10300, 'quality': '720p'}, '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.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 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)) 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 # 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 '*'))) ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
if ignore_file: if ignore_file:
return True 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()) log.error('Rar modify date enabled, but failed: %s', traceback.format_exc())
extr_files.append(extr_file_path) extr_files.append(extr_file_path)
del rar_handle 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: 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()))
continue continue

1
couchpotato/core/plugins/scanner.py

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

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

@ -19,7 +19,7 @@ name_scores = [
# Audio # Audio
'dts:4', 'ac3:2', 'dts:4', 'ac3:2',
# Quality # 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 # Language / Subs
'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10', 'dutch:-10', 'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10', 'dutch:-10',
# Release groups # 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.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat
from couchpotato.core.softchroot import SoftChroot
class Settings(object): class Settings(object):
@ -77,7 +77,8 @@ class Settings(object):
return self.p return self.p
def sections(self): def sections(self):
return self.p.sections() res = filter( self.isSectionReadable, self.p.sections())
return res
def connectEvents(self): def connectEvents(self):
addEvent('settings.options', self.addOptions) addEvent('settings.options', self.addOptions)
@ -106,9 +107,21 @@ class Settings(object):
self.save() self.save()
def set(self, section, option, value): 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) return self.p.set(section, option, value)
def get(self, option = '', section = 'core', default = None, type = None): 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:
try: type = self.types[section][option] try: type = self.types[section][option]
@ -123,6 +136,14 @@ class Settings(object):
return default return default
def delete(self, option = '', section = 'core'): 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.p.remove_option(section, option)
self.save() self.save()
@ -153,11 +174,30 @@ class Settings(object):
def getValues(self): def getValues(self):
values = {} 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(): for section in self.sections():
# COMMENTED_SKIPPING
#if not self.isSectionReadable(section):
# continue
values[section] = {} values[section] = {}
for option in self.p.items(section): for option in self.p.items(section):
(option_name, option_value) = option (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 is_password = False
try: is_password = self.types[section][option_name] == 'password' try: is_password = self.types[section][option_name] == 'password'
except: pass except: pass
@ -189,14 +229,52 @@ class Settings(object):
self.types[section][option] = type self.types[section][option] = type
def addOptions(self, section_name, options): def addOptions(self, section_name, options):
# no additional actions (related to ro-rw options) are required here
if not self.options.get(section_name): if not self.options.get(section_name):
self.options[section_name] = options self.options[section_name] = options
else: else:
self.options[section_name] = mergeDicts(self.options[section_name], options) self.options[section_name] = mergeDicts(self.options[section_name], options)
def getOptions(self): 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): def view(self, **kwargs):
return { return {
@ -210,6 +288,11 @@ class Settings(object):
option = kwargs.get('name') option = kwargs.get('name')
value = kwargs.get('value') 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 # See if a value handler is attached, use that as value
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True) 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) fireEvent('setting.save.%s.*.after' % section, single = True)
return { 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): def getProperty(self, identifier):
from couchpotato import get_db 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 = CPLog(__name__)
log.debug('Started with options %s', options) 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 # Check available space
try: try:
total_space, available_space = getFreeSpace(data_dir) 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)); window.addEvent("resize", self.resize.bind(self));
self.resize(); self.resize();
self.checkCache();
}, },
checkCache: function() { checkCache: function() {
window.addEventListener("load", function() { window.addEventListener("load", function() {
@ -1336,12 +1335,14 @@ var OptionBase = new Class({
klass: "", klass: "",
focused_class: "focused", focused_class: "focused",
save_on_change: true, save_on_change: true,
read_only: false,
initialize: function(section, name, value, options) { initialize: function(section, name, value, options) {
var self = this; var self = this;
self.setOptions(options); self.setOptions(options);
self.section = section; self.section = section;
self.name = name; self.name = name;
self.value = self.previous_value = value; self.value = self.previous_value = value;
self.read_only = !(options && options.writable);
self.createBase(); self.createBase();
self.create(); self.create();
self.createHint(); self.createHint();
@ -1354,7 +1355,7 @@ var OptionBase = new Class({
}, },
createBase: function() { createBase: function() {
var self = this; 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() {}, create: function() {},
createLabel: function() { createLabel: function() {
@ -1393,7 +1394,11 @@ var OptionBase = new Class({
} }
}, },
save: function() { 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); App.fireEvent("setting.save." + self.section + "." + self.name, value);
Api.request("settings.save", { Api.request("settings.save", {
data: { data: {
@ -1451,7 +1456,9 @@ Option.String = new Class({
type: "text", type: "text",
name: self.postName(), name: self.postName(),
value: self.getSettingValue(), value: self.getSettingValue(),
placeholder: self.getPlaceholder() placeholder: self.getPlaceholder(),
readonly: self.read_only,
disabled: self.read_only
})); }));
}, },
getPlaceholder: function() { getPlaceholder: function() {
@ -1464,7 +1471,9 @@ Option.Dropdown = new Class({
create: function() { create: function() {
var self = this; var self = this;
self.el.adopt(self.createLabel(), new Element("div.select_wrapper.icon-dropdown").grab(self.input = new Element("select", { 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) { Object.each(self.options.values, function(value) {
new Element("option", { new Element("option", {
@ -1486,7 +1495,9 @@ Option.Checkbox = new Class({
name: self.postName(), name: self.postName(),
type: "checkbox", type: "checkbox",
checked: self.getSettingValue(), checked: self.getSettingValue(),
id: randomId id: randomId,
readonly: self.read_only,
disabled: self.read_only
})); }));
}, },
getValue: function() { getValue: function() {
@ -1504,7 +1515,9 @@ Option.Password = new Class({
type: "text", type: "text",
name: self.postName(), name: self.postName(),
value: self.getSettingValue() ? "********" : "", value: self.getSettingValue() ? "********" : "",
placeholder: self.getPlaceholder() placeholder: self.getPlaceholder(),
readonly: self.read_only,
disabled: self.read_only
})); }));
self.input.addEvent("focus", function() { self.input.addEvent("focus", function() {
self.input.set("value", ""); 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", { self.el.adopt(new Element("label.switch").adopt(self.input = new Element("input", {
type: "checkbox", type: "checkbox",
checked: self.getSettingValue(), checked: self.getSettingValue(),
id: "r-" + randomString() id: "r-" + randomString(),
readonly: self.read_only,
disabled: self.read_only
}), new Element("div.toggle"))); }), new Element("div.toggle")));
}, },
changed: function() { changed: function() {
@ -1561,12 +1576,23 @@ Option.Directory = new Class({
current_dir: "", current_dir: "",
create: function() { create: function() {
var self = this; 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", { self.el.adopt(self.createLabel(), self.directory_inlay = new Element("span.directory", {
events: { events: {
click: self.showBrowser.bind(self) click: self.showBrowser.bind(self)
} }
}).adopt(self.input = new Element("input", { }).adopt(self.input = new Element("input", {
value: self.getSettingValue(), value: self.getSettingValue(),
readonly: self.read_only,
disabled: self.read_only,
events: { events: {
change: self.filterDirectory.bind(self), change: self.filterDirectory.bind(self),
keydown: function(e) { keydown: function(e) {
@ -1576,6 +1602,7 @@ Option.Directory = new Class({
paste: self.filterDirectory.bind(self) paste: self.filterDirectory.bind(self)
} }
}))); })));
}
self.cached = {}; self.cached = {};
}, },
filterDirectory: function(e) { filterDirectory: function(e) {
@ -1992,14 +2019,16 @@ var AboutSettingTab = new Class({
self.createAbout(); self.createAbout();
}); });
self.settings.default_action = "about"; self.settings.default_action = "about";
self.hide_dirs = !!App.options && App.options.hide_about_dirs;
}, },
createAbout: function() { createAbout: function() {
var self = this; var self = this;
var millennium = new Date(2008, 7, 16), today = new Date(), one_day = 1e3 * 60 * 60 * 24; var millennium = new Date(2008, 7, 16), today = new Date(), one_day = 1e3 * 60 * 60 * 24;
var about_block;
self.settings.createGroup({ self.settings.createGroup({
label: "About This CouchPotato", label: "About This CouchPotato",
name: "variables" 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...", text: "Getting version...",
events: { events: {
click: App.checkForUpdate.bind(App, function(json) { 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", { }), new Element("dt[text=Updater]"), self.updater_type = new Element("dd.updater"), new Element("dt[text=ID]"), new Element("dd", {
text: App.getOption("pid") 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") text: App.getOption("app_dir")
}), new Element("dd", { }), new Element("dd", {
text: App.getOption("data_dir") text: App.getOption("data_dir")
@ -2022,7 +2053,8 @@ var AboutSettingTab = new Class({
html: App.getOption("args") html: App.getOption("args")
}), new Element("dd", { }), new Element("dd", {
html: App.getOption("options") html: App.getOption("options")
}))); }));
}
if (!self.fillVersion(Updater.getInfo())) Updater.addEvent("loaded", self.fillVersion.bind(self)); if (!self.fillVersion(Updater.getInfo())) Updater.addEvent("loaded", self.fillVersion.bind(self));
self.settings.createGroup({ self.settings.createGroup({
name: "Help Support CouchPotato" 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.settings.default_action = 'about';
self.hide_dirs = !! App.options && App.options.hide_about_dirs;
}, },
createAbout: function(){ createAbout: function(){
@ -38,11 +38,13 @@ var AboutSettingTab = new Class({
today = new Date(), today = new Date(),
one_day = 1000*60*60*24; one_day = 1000*60*60*24;
var about_block;
self.settings.createGroup({ self.settings.createGroup({
'label': 'About This CouchPotato', 'label': 'About This CouchPotato',
'name': 'variables' 'name': 'variables'
}).inject(self.content).adopt( }).inject(self.content).adopt(
new Element('dl.info').adopt( (about_block = new Element('dl.info')).adopt(
new Element('dt[text=Version]'), new Element('dt[text=Version]'),
self.version_text = new Element('dd.version', { self.version_text = new Element('dd.version', {
'text': 'Getting version...', 'text': 'Getting version...',
@ -58,18 +60,25 @@ var AboutSettingTab = new Class({
} }
} }
}), }),
new Element('dt[text=Updater]'), new Element('dt[text=Updater]'),
self.updater_type = new Element('dd.updater'), self.updater_type = new Element('dd.updater'),
new Element('dt[text=ID]'), 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('dt[text=Directories]'),
new Element('dd', {'text': App.getOption('app_dir')}), new Element('dd', {'text': App.getOption('app_dir')}),
new Element('dd', {'text': App.getOption('data_dir')}), new Element('dd', {'text': App.getOption('data_dir')}),
new Element('dt[text=Startup Args]'), new Element('dt[text=Startup Args]'),
new Element('dd', {'html': App.getOption('args')}), new Element('dd', {'html': App.getOption('args')}),
new Element('dd', {'html': App.getOption('options')}) new Element('dd', {'html': App.getOption('options')})
)
); );
}
if(!self.fillVersion(Updater.getInfo())) if(!self.fillVersion(Updater.getInfo()))
Updater.addEvent('loaded', self.fillVersion.bind(self)); Updater.addEvent('loaded', self.fillVersion.bind(self));

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

@ -334,6 +334,7 @@ var OptionBase = new Class({
klass: '', klass: '',
focused_class: 'focused', focused_class: 'focused',
save_on_change: true, save_on_change: true,
read_only: false,
initialize: function(section, name, value, options){ initialize: function(section, name, value, options){
var self = this; var self = this;
@ -342,6 +343,7 @@ var OptionBase = new Class({
self.section = section; self.section = section;
self.name = name; self.name = name;
self.value = self.previous_value = value; self.value = self.previous_value = value;
self.read_only = !(options && options.writable);
self.createBase(); self.createBase();
self.create(); self.create();
@ -363,7 +365,11 @@ var OptionBase = new Class({
*/ */
createBase: function(){ createBase: function(){
var self = this; 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(){ create: function(){
@ -418,7 +424,13 @@ var OptionBase = new Class({
save: function(){ save: function(){
var self = this, 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); App.fireEvent('setting.save.'+self.section+'.'+self.name, value);
@ -493,7 +505,9 @@ Option.String = new Class({
'type': 'text', 'type': 'text',
'name': self.postName(), 'name': self.postName(),
'value': self.getSettingValue(), '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(), self.createLabel(),
new Element('div.select_wrapper.icon-dropdown').grab( new Element('div.select_wrapper.icon-dropdown').grab(
self.input = new Element('select', { 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(), 'name': self.postName(),
'type': 'checkbox', 'type': 'checkbox',
'checked': self.getSettingValue(), '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', 'type': 'text',
'name': self.postName(), 'name': self.postName(),
'value': self.getSettingValue() ? '********' : '', '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', { self.input = new Element('input', {
'type': 'checkbox', 'type': 'checkbox',
'checked': self.getSettingValue(), 'checked': self.getSettingValue(),
'id': 'r-'+randomString() 'id': 'r-'+randomString(),
'readonly' : self.read_only,
'disabled' : self.read_only,
}), }),
new Element('div.toggle') new Element('div.toggle')
) )
@ -652,7 +674,19 @@ Option.Directory = new Class({
create: function(){ create: function(){
var self = this; 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.el.adopt(
self.createLabel(), self.createLabel(),
self.directory_inlay = new Element('span.directory', { self.directory_inlay = new Element('span.directory', {
@ -662,6 +696,8 @@ Option.Directory = new Class({
}).adopt( }).adopt(
self.input = new Element('input', { self.input = new Element('input', {
'value': self.getSettingValue(), 'value': self.getSettingValue(),
'readonly' : self.read_only,
'disabled' : self.read_only,
'events': { 'events': {
'change': self.filterDirectory.bind(self), 'change': self.filterDirectory.bind(self),
'keydown': function(e){ 'keydown': function(e){
@ -674,6 +710,7 @@ Option.Directory = new Class({
}) })
) )
); );
}
self.cached = {}; 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{font-size:12px}
.search_form .results_container .results .media_result .options{left:0} .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>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%} .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} .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} @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} @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} .page.home .search_form .wrapper .results_container .results .media_result{height:66px}
} .page.home .search_form .wrapper .results_container .results .media_result .thumbnail{width:40px}
@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 .options{left: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} .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} .big_search{background:#ebebeb}
.dark .big_search{background:#353535} .dark .big_search{background:#353535}
.page.movies{bottom:auto;z-index:21;height:80px} .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} @media (max-width:480px){.page.movies{height:44px}
.page.movies_manage,.page.movies_wanted{top: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,.page.movies_wanted .empty_manage{padding:20px}
.page.movies_manage .empty_manage .after_manage,.page.movies_wanted .empty_manage .after_manage{margin-top: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} .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} @media (max-width:480px){.with_navigation .movie input[type=checkbox]{display:none}
} }
.with_navigation .movie input[type=checkbox]:hover{opacity:1!important} .with_navigation .movie input[type=checkbox]:hover{opacity:1!important}
@ -145,6 +143,7 @@
.movies{position:relative} .movies{position:relative}
.movies .no_movies{display:block;padding:20px} .movies .no_movies{display:block;padding:20px}
@media (max-width:768px){.movies .no_movies{padding:10px} @media (max-width:768px){.movies .no_movies{padding:10px}
.movies>.description{display:none}
} }
.movies .no_movies a{color:#ac0000} .movies .no_movies a{color:#ac0000}
.dark .movies .no_movies a{color:#f85c22} .dark .movies .no_movies a{color:#f85c22}
@ -153,10 +152,9 @@
.dark .movies .message a{color:#f85c22} .dark .movies .message a{color:#f85c22}
.movies.movies>h2{padding:0 20px;line-height:80px} .movies.movies>h2{padding:0 20px;line-height:80px}
@media (max-width:480px){.movies.movies>h2{line-height:44px;padding:0 10px} @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} .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} .movies>.description a{color:#ac0000;display:inline}
.dark .movies>.description a{color:#f85c22} .dark .movies>.description a{color:#f85c22}
.movies>.loading{background:#FFF} .movies>.loading{background:#FFF}
@ -179,7 +177,7 @@
.list_list .movie .poster{display:none} .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{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{-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%} @media (max-width:768px){.movies .progress div{width:100%}
.list_list .movie .info{display:block;padding:10px} .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} .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 .title .year{display:inline-block;margin:0 10px;opacity:.5}
.list_list .movie .info .eta{font-size:.8em;opacity:.5;margin-right:4px} .list_list .movie .info .eta{font-size:.8em;opacity:.5;margin-right:4px}
@media (max-width:480px){.movies .movie .actions{pointer-events:none} @media (max-width:480px){.list_list .movie .info .eta{display:none}
.list_list .movie .info .eta{display:none}
} }
.list_list .movie .info .quality{clear:both;overflow:hidden} .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} .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 .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{white-space:nowrap;overflow:hidden;font-size:.9em}
.thumb_list .movie .info .quality span{font-size:.8em;margin-right:2px} .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%)} .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{position:relative;margin-right:10px;float:right;clear:both}
.thumb_list .movie .actions .action:first-child{margin-top:10px} .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} @media (max-width:480px){.thumb_list .movie:hover .actions{display:none}
.page.movie_details{left:0} .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} .page.movie_details .overlay .ripple{background:#FFF}
@media (max-width:480px){.page.movie_details .overlay{left:0;border-radius:0;transition:none} @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 .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)} .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} .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{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{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{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} .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} .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: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 .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} .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} @media (max-width:480px){.page.movie_details .scroll_content>.head .more_menu>a{line-height:44px}
.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{top:25px}
} }
.page.movie_details .scroll_content>.head .more_menu .wrapper:before{top:0;left:auto;right:22px} .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 .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{pointer-events:auto}
.page.movie_details.show .overlay{opacity:1;transition-delay:0s} .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.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{text-align:right;font-style:italic;font-size:.9em}
.page.movie_details .section_description .meta span{display:inline-block;margin:10px 10px 0} .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 .name{width:100%;font-weight:700}
.page.movie_details .releases .item.head{display:none} .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{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} .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} .dark .page.movie_details .releases .item .actions a{color:#FFF}
.page.movie_details .releases .item .actions a:hover{color:#ac0000} .page.movie_details .releases .item .actions a:hover{color:#ac0000}
.dark .page.movie_details .releases .item .actions a:hover{color:#f85c22} .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} .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} @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-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-download:after{content:"download"}
.page.movie_details .releases .item .actions a.icon-cancel:after{content:"ignore"} .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{min-width:70px;max-width:70px}
.page.movie_details .releases .status:before{content:"Status:"} .page.movie_details .releases .status:before{content:"Status:"}
@ -400,16 +398,15 @@
.dark .page.movie_details .section_trailer.section_trailer{background:#111} .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.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{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 .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: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} @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{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{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: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} @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: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} .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{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} .dark .alph_nav .mass_edit_form{background:#2d2d2d}
.mass_editing .alph_nav .mass_edit_form{max-height:44px} .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} .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} .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} .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} @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} .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 a{display:inline-block}
.alph_nav .menus .actions .active{display:none} .alph_nav .menus .actions .active{display:none}
.alph_nav .menus .filter .wrapper{width:320px} .alph_nav .menus .filter .wrapper{width:320px}
.alph_nav .menus .filter .button{margin-top:-2px} .alph_nav .menus .filter .button{margin-top:-2px}
.alph_nav .menus .filter .search{position:relative} .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: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} .dark .alph_nav .menus .filter .search input{background:#2d2d2d;border-color:#353535}
.alph_nav .menus .filter .numbers{padding:10px} .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} .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} @media (max-width:480px){.header .navigation .logo{font-size:28px;line-height:44px;height:44px}
.header .navigation .logo:after{content:'CP'} .header .navigation .logo:after{content:'CP'}
.header .navigation .logo span{display:none} .header .navigation .logo span{display:none}
.header .navigation ul li{line-height:0}
} }
.header .navigation ul{padding:0;margin:0} .header .navigation ul{padding:0;margin:0}
.header .navigation ul li{display:block} .header .navigation ul li{display:block}
.header .navigation ul li a{padding:10px 20px;display:block;position:relative} .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} .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} @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{line-height:24px;height:44px;padding:10px 0;text-align:center}
.header .navigation ul li a span{display:none} .header .navigation ul li a span{display:none}
.header .navigation ul li a:before{display:block} .header .navigation ul li a:before{display:block}
} }
.header .navigation ul li a.icon-home:before{font-size:24px} .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} .header .donate:before{display:none;font-size:20px;text-align:center}
@media (max-width:480px){.header .donate{bottom:132px;padding:10px 0} @media (max-width:480px){.header .donate{bottom:132px;padding:10px 0}
.header .donate span{display:none} .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{border-color:#FFF;color:#FFF;transition:none}
.question a:hover{background:#ac0000;color:#FFF} .question a:hover{background:#ac0000;color:#FFF}
.dark .question a:hover{background:#f85c22} .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,.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{color:#FFF;text-align:center;width:320px;margin:-49px 0 0 -160px;font-size:16px}
.mask .message h1{font-size:1.5em} .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} @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,.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 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{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} .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{-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} .page.settings fieldset .ctrlHolder .formHint a{font-weight:400;color:#ac0000;text-decoration:underline}
.dark .page.settings fieldset .ctrlHolder .formHint a{color:#f85c22} .dark .page.settings fieldset .ctrlHolder .formHint a{color:#f85c22}
@media (max-width:480px){.page.settings fieldset .ctrlHolder .select_wrapper{width:100%} @media (max-width:480px){.page.settings fieldset .ctrlHolder .formHint{min-width:100%;margin-left:0}
.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 a{margin:0}
.page.settings fieldset .ctrlHolder.test_button .success{margin-left:10px} .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{display:none}
.page.settings fieldset.disabled>.ctrlHolder:first-child{display:-webkit-flex;display:-ms-flexbox;display:flex} .page.settings fieldset.disabled>.ctrlHolder:first-child{display:-webkit-flex;display:-ms-flexbox;display:flex}
.page.settings fieldset.enabler{display:block} .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} .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} @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 .info dd.version{cursor:pointer}
.page.settings .tab_about .group_actions>div{padding:20px;text-align:center} .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} .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 .directory input{width:100%}
.page.settings .multi_directory .delete{color:#ac0000;padding:0 10px;opacity:.6;font-size:1.5em} .page.settings .multi_directory .delete{color:#ac0000;padding:0 10px;opacity:.6;font-size:1.5em}
.dark .page.settings .multi_directory .delete{color:#f85c22} .dark .page.settings .multi_directory .delete{color:#f85c22}

16
couchpotato/static/style/settings.scss

@ -275,6 +275,22 @@
margin-left: $padding / 2; 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 { &.disabled {

3
couchpotato/templates/index.html

@ -85,7 +85,8 @@
'app_dir': {{ json_encode(Env.get('app_dir', unicode = True)) }}, 'app_dir': {{ json_encode(Env.get('app_dir', unicode = True)) }},
'data_dir': {{ json_encode(Env.get('data_dir', unicode = True)) }}, '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)) }},
'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. higher level interface which makes some common operations easier.
""" """
__version__ = '0.99.3' __version__ = '0.99.6'
try: try:
WindowsError WindowsError
@ -159,6 +159,12 @@ class RarFile(RarFileImplementation):
checker = condition2checker(condition) checker = condition2checker(condition)
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite) 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): def condition2checker(condition):
"""Converts different condition types to callback""" """Converts different condition types to callback"""
if type(condition) in [str, unicode]: if type(condition) in [str, unicode]:

79
libs/unrar2/unix.py

@ -23,14 +23,16 @@
# Unix version uses unrar command line executable # Unix version uses unrar command line executable
import platform import platform
import stat import stat
import subprocess import subprocess
import gc import gc
import os
import os.path import os, os.path
import time import time, re
import re
from rar_exceptions import * from rar_exceptions import *
from dateutil.parser import parse
from rar_exceptions import *
class UnpackerNotInstalled(Exception): pass class UnpackerNotInstalled(Exception): pass
@ -53,7 +55,7 @@ def call_unrar(params, custom_path = None):
for command in (custom_path, 'unrar', 'rar', osx_unrar): for command in (custom_path, 'unrar', 'rar', osx_unrar):
if not command: continue 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
break break
except OSError: except OSError:
@ -65,7 +67,7 @@ def call_unrar(params, custom_path = None):
args = [rar_executable_cached] + params args = [rar_executable_cached] + params
try: try:
gc.disable() # See http://bugs.python.org/issue1336 gc.disable() # See http://bugs.python.org/issue1336
return subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.PIPE) return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
finally: finally:
gc.enable() gc.enable()
@ -81,18 +83,18 @@ class RarFileImplementation(object):
for line in stderrdata.splitlines(): for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"): if line.strip().startswith("Cannot open"):
raise FileOpenError raise FileOpenError
if line.find("CRC failed") >= 0: if line.find("CRC failed")>=0:
raise IncorrectRARPassword raise IncorrectRARPassword
accum = [] accum = []
source = iter(stdoutdata.splitlines()) source = iter(stdoutdata.splitlines())
line = '' line = ''
while not (line.startswith('UNRAR')): while (line.find('RAR ') == -1):
line = source.next() line = source.next()
signature = line signature = line
# The code below is mighty flaky # The code below is mighty flaky
# and will probably crash on localized versions of RAR # and will probably crash on localized versions of RAR
# but I see no safe way to rewrite it using a CLI tool # 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 rar_executable_version = 4
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')): while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
if line.strip().endswith('is not RAR archive'): if line.strip().endswith('is not RAR archive'):
@ -106,7 +108,7 @@ class RarFileImplementation(object):
self.comment = '\n'.join(accum[:-1]) self.comment = '\n'.join(accum[:-1])
else: else:
self.comment = None self.comment = None
elif signature.startswith("UNRAR 5"): elif signature.find("RAR 5") > -1:
rar_executable_version = 5 rar_executable_version = 5
line = source.next() line = source.next()
while not line.startswith('Archive:'): while not line.startswith('Archive:'):
@ -127,9 +129,9 @@ class RarFileImplementation(object):
return '-' if self.password == None else self.password return '-' if self.password == None else self.password
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, self.custom_path) return call_unrar([cmd] + soptions + ['--', self.archiveName] + files, self.custom_path)
def infoiter(self): def infoiter(self):
@ -156,7 +158,7 @@ class RarFileImplementation(object):
if rar_executable_version == 4: if rar_executable_version == 4:
while not line.startswith('-----------'): while not line.startswith('-----------'):
accum.append(line) accum.append(line)
if len(accum) == 2: if len(accum)==2:
data = {} data = {}
data['index'] = i data['index'] = i
# asterisks mark password-encrypted files # asterisks mark password-encrypted files
@ -165,8 +167,9 @@ class RarFileImplementation(object):
data['size'] = int(fields[0]) data['size'] = int(fields[0])
attr = fields[5] attr = fields[5]
data['isdir'] = 'd' in attr.lower() data['isdir'] = 'd' in attr.lower()
data['datetime'] = time.strptime(fields[3] + " " + fields[4], '%d-%m-%y %H:%M') data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M')
data['comment'] = None data['comment'] = None
data['volume'] = None
yield data yield data
accum = [] accum = []
i += 1 i += 1
@ -180,8 +183,9 @@ class RarFileImplementation(object):
data['size'] = int(fields[1]) data['size'] = int(fields[1])
attr = fields[0] attr = fields[0]
data['isdir'] = 'd' in attr.lower() 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['comment'] = None
data['volume'] = None
yield data yield data
i += 1 i += 1
line = source.next() line = source.next()
@ -191,7 +195,7 @@ class RarFileImplementation(object):
res = [] res = []
for info in self.infoiter(): for info in self.infoiter():
checkres = checker(info) checkres = checker(info)
if checkres == True and not info.isdir: if checkres==True and not info.isdir:
pipe = self.call('p', ['inul'], [info.filename]).stdout pipe = self.call('p', ['inul'], [info.filename]).stdout
res.append((info, pipe.read())) res.append((info, pipe.read()))
return res return res
@ -214,17 +218,54 @@ class RarFileImplementation(object):
checkres = checker(info) checkres = checker(info)
if type(checkres) in [str, unicode]: if type(checkres) in [str, unicode]:
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows") raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows")
if checkres == True and not info.isdir: if checkres==True and not info.isdir:
names.append(info.filename) names.append(info.filename)
res.append(info) res.append(info)
names.append(path) names.append(path)
proc = self.call(command, options, names) proc = self.call(command, options, names)
stdoutdata, stderrdata = proc.communicate() stdoutdata, stderrdata = proc.communicate()
if stderrdata.find("CRC failed") >= 0 or stderrdata.find("Checksum error") >= 0: if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
raise IncorrectRARPassword raise IncorrectRARPassword
return res return res
def destruct(self): def destruct(self):
pass 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 __future__ import generators
from couchpotato.environment import Env from couchpotato.environment import Env
from shutil import copyfile from shutil import copyfile
import ctypes.wintypes
import os.path import ctypes, ctypes.wintypes
import os, os.path, re
import time import time
from rar_exceptions import * from rar_exceptions import *
@ -43,6 +44,7 @@ ERAR_EREAD = 18
ERAR_EWRITE = 19 ERAR_EWRITE = 19
ERAR_SMALL_BUF = 20 ERAR_SMALL_BUF = 20
ERAR_UNKNOWN = 21 ERAR_UNKNOWN = 21
ERAR_MISSING_PASSWORD = 22
RAR_OM_LIST = 0 RAR_OM_LIST = 0
RAR_OM_EXTRACT = 1 RAR_OM_EXTRACT = 1
@ -75,8 +77,12 @@ if os.path.isfile(dll_copy):
copyfile(dll_file, 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): class RAROpenArchiveDataEx(ctypes.Structure):
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST): def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
@ -193,7 +199,7 @@ class RarInfoIterator(object):
self.index = 0 self.index = 0
self.headerData = RARHeaderDataEx() self.headerData = RARHeaderDataEx()
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) 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 raise IncorrectRARPassword
self.arc.lockStatus = "locked" self.arc.lockStatus = "locked"
self.arc.needskip = False self.arc.needskip = False
@ -213,7 +219,7 @@ class RarInfoIterator(object):
data = {} data = {}
data['index'] = self.index data['index'] = self.index
data['filename'] = self.headerData.FileName data['filename'] = self.headerData.FileNameW
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime) data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0) data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32) data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
@ -257,6 +263,7 @@ class RarFileImplementation(object):
self.lockStatus = "ready" self.lockStatus = "ready"
self.isVolume = archiveData.Flags & 1
def destruct(self): def destruct(self):
@ -282,7 +289,7 @@ class RarFileImplementation(object):
c_callback = UNRARCALLBACK(reader._callback) c_callback = UNRARCALLBACK(reader._callback)
RARSetCallback(self._handle, c_callback, 1) RARSetCallback(self._handle, c_callback, 1)
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None) tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
if tmpres==ERAR_BAD_DATA: if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword raise IncorrectRARPassword
self.needskip = False self.needskip = False
res.append((info, reader.get_result())) res.append((info, reader.get_result()))
@ -304,11 +311,29 @@ class RarFileImplementation(object):
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)
if tmpres==ERAR_BAD_DATA: if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword raise IncorrectRARPassword
self.needskip = False self.needskip = False
res.append(info) res.append(info)
return res 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": { "devDependencies": {
"grunt": "~0.4.5", "grunt": "~0.4.5",
"grunt-autoprefixer": "^3.0.3", "grunt-autoprefixer": "^3.0.3",
"grunt-concurrent": "~2.0.1", "grunt-concurrent": "~2.1.0",
"grunt-contrib-clean": "^0.6.0", "grunt-contrib-clean": "^0.7.0",
"grunt-contrib-cssmin": "~0.13.0", "grunt-contrib-cssmin": "~0.14.0",
"grunt-contrib-jshint": "~0.11.2", "grunt-contrib-jshint": "~0.11.2",
"grunt-contrib-sass": "^0.9.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-contrib-watch": "~0.6.1",
"grunt-shell-spawn": "^0.3.8", "grunt-shell-spawn": "^0.3.8",
"jit-grunt": "^0.9.1", "jit-grunt": "^0.9.1",

Loading…
Cancel
Save