Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -16,51 +16,19 @@ class Scheduler(Plugin):
|
|||||||
|
|
||||||
addEvent('schedule.cron', self.cron)
|
addEvent('schedule.cron', self.cron)
|
||||||
addEvent('schedule.interval', self.interval)
|
addEvent('schedule.interval', self.interval)
|
||||||
addEvent('schedule.start', self.start)
|
addEvent('schedule.remove', self.remove)
|
||||||
addEvent('schedule.restart', self.start)
|
|
||||||
|
|
||||||
addEvent('app.load', self.start)
|
|
||||||
|
|
||||||
self.sched = Sched(misfire_grace_time = 60)
|
self.sched = Sched(misfire_grace_time = 60)
|
||||||
|
|
||||||
def remove(self, identifier):
|
|
||||||
for type in ['interval', 'cron']:
|
|
||||||
try:
|
|
||||||
self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
|
|
||||||
log.debug('%s unscheduled %s', (type.capitalize(), identifier))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
|
|
||||||
# Stop all running
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
# Crons
|
|
||||||
for identifier in self.crons:
|
|
||||||
try:
|
|
||||||
self.remove(identifier)
|
|
||||||
cron = self.crons[identifier]
|
|
||||||
job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
|
|
||||||
cron['job'] = job
|
|
||||||
except ValueError, e:
|
|
||||||
log.error('Failed adding cronjob: %s', e)
|
|
||||||
|
|
||||||
# Intervals
|
|
||||||
for identifier in self.intervals:
|
|
||||||
try:
|
|
||||||
self.remove(identifier)
|
|
||||||
interval = self.intervals[identifier]
|
|
||||||
job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
|
|
||||||
interval['job'] = job
|
|
||||||
except ValueError, e:
|
|
||||||
log.error('Failed adding interval cronjob: %s', e)
|
|
||||||
|
|
||||||
# Start it
|
|
||||||
log.debug('Starting scheduler')
|
|
||||||
self.sched.start()
|
self.sched.start()
|
||||||
self.started = True
|
self.started = True
|
||||||
log.debug('Scheduler started')
|
|
||||||
|
def remove(self, identifier):
|
||||||
|
for cron_type in ['intervals', 'crons']:
|
||||||
|
try:
|
||||||
|
self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
|
||||||
|
log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def doShutdown(self):
|
def doShutdown(self):
|
||||||
super(Scheduler, self).doShutdown()
|
super(Scheduler, self).doShutdown()
|
||||||
@@ -82,6 +50,7 @@ class Scheduler(Plugin):
|
|||||||
'day': day,
|
'day': day,
|
||||||
'hour': hour,
|
'hour': hour,
|
||||||
'minute': minute,
|
'minute': minute,
|
||||||
|
'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
|
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
|
||||||
@@ -93,4 +62,5 @@ class Scheduler(Plugin):
|
|||||||
'hours': hours,
|
'hours': hours,
|
||||||
'minutes': minutes,
|
'minutes': minutes,
|
||||||
'seconds': seconds,
|
'seconds': seconds,
|
||||||
|
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class Updater(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.updater = SourceUpdater()
|
self.updater = SourceUpdater()
|
||||||
|
|
||||||
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
|
||||||
addEvent('app.load', self.autoUpdate)
|
addEvent('app.load', self.autoUpdate)
|
||||||
addEvent('updater.info', self.info)
|
addEvent('updater.info', self.info)
|
||||||
|
|
||||||
@@ -52,6 +51,15 @@ class Updater(Plugin):
|
|||||||
'return': {'type': 'see updater.info'}
|
'return': {'type': 'see updater.info'}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addEvent('setting.save.updater.enabled.after', self.setCrons)
|
||||||
|
|
||||||
|
def setCrons(self):
|
||||||
|
|
||||||
|
fireEvent('schedule.remove', 'updater.check', single = True)
|
||||||
|
if self.isEnabled():
|
||||||
|
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||||
|
self.autoUpdate() # Check after enabling
|
||||||
|
|
||||||
def autoUpdate(self):
|
def autoUpdate(self):
|
||||||
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||||
if self.updater.doUpdate():
|
if self.updater.doUpdate():
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ class StatusList(list):
|
|||||||
'id': 0,
|
'id': 0,
|
||||||
'status': 'busy',
|
'status': 'busy',
|
||||||
'downloader': self.provider.getName(),
|
'downloader': self.provider.getName(),
|
||||||
|
'folder': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeDicts(defaults, result)
|
return mergeDicts(defaults, result)
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class NZBGet(Downloader):
|
|||||||
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
|
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
|
||||||
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
||||||
'timeleft': str(timedelta(seconds = 0)),
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
|
'folder': item['DestDir']
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ class NZBVortex(Downloader):
|
|||||||
'name': item['uiTitle'],
|
'name': item['uiTitle'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['state'],
|
'original_status': item['state'],
|
||||||
'timeleft':-1,
|
'timeleft': -1,
|
||||||
|
'folder': item['destinationPath'],
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
|||||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
from datetime import timedelta
|
||||||
from urllib2 import URLError
|
from urllib2 import URLError
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
@@ -46,19 +47,15 @@ class Sabnzbd(Downloader):
|
|||||||
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if sab_data.get('error'):
|
|
||||||
log.error('Error getting data from SABNZBd: %s', sab_data.get('error'))
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.debug('Result from SAB: %s', sab_data)
|
log.debug('Result from SAB: %s', sab_data)
|
||||||
if sab_data.get('status'):
|
if sab_data.get('status') and not sab_data.get('error'):
|
||||||
log.info('NZB sent to SAB successfully.')
|
log.info('NZB sent to SAB successfully.')
|
||||||
if filedata:
|
if filedata:
|
||||||
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
|
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
log.error(sab_data)
|
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def getAllDownloadStatus(self):
|
def getAllDownloadStatus(self):
|
||||||
@@ -109,7 +106,8 @@ class Sabnzbd(Downloader):
|
|||||||
'name': item['name'],
|
'name': item['name'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['status'],
|
'original_status': item['status'],
|
||||||
'timeleft': 0,
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
|
'folder': item['storage'],
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -41,15 +41,22 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'directory',
|
'name': 'directory',
|
||||||
'type': 'directory',
|
'type': 'directory',
|
||||||
'description': 'Where should Transmission saved the downloaded files?',
|
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'ratio',
|
'name': 'ratio',
|
||||||
'default': 10,
|
'default': 10,
|
||||||
'type': 'int',
|
'type': 'float',
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Stop transfer when reaching ratio',
|
'description': 'Stop transfer when reaching ratio',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'ratiomode',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'int',
|
||||||
|
'advanced': True,
|
||||||
|
'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'manual',
|
'name': 'manual',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from datetime import timedelta
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
|
import traceback
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -18,7 +22,7 @@ class Transmission(Downloader):
|
|||||||
|
|
||||||
def download(self, data, movie, filedata = None):
|
def download(self, data, movie, filedata = None):
|
||||||
|
|
||||||
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||||
|
|
||||||
# Load host from config and split out port.
|
# Load host from config and split out port.
|
||||||
host = self.conf('host').split(':')
|
host = self.conf('host').split(':')
|
||||||
@@ -27,22 +31,24 @@ class Transmission(Downloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Set parameters for Transmission
|
# Set parameters for Transmission
|
||||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
|
||||||
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
|
||||||
|
|
||||||
# Create the empty folder to download too
|
|
||||||
self.makeDir(folder_path)
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'paused': self.conf('paused', default = 0),
|
'paused': self.conf('paused', default = 0),
|
||||||
'download-dir': folder_path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(self.conf('directory', default = '')) > 0:
|
||||||
|
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||||
|
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||||
|
|
||||||
|
# Create the empty folder to download too
|
||||||
|
self.makeDir(folder_path)
|
||||||
|
|
||||||
|
params['download-dir'] = folder_path
|
||||||
|
|
||||||
torrent_params = {}
|
torrent_params = {}
|
||||||
if self.conf('ratio'):
|
if self.conf('ratio'):
|
||||||
torrent_params = {
|
torrent_params = {
|
||||||
'seedRatioLimit': self.conf('ratio'),
|
'seedRatioLimit': self.conf('ratio'),
|
||||||
'seedRatioMode': self.conf('ratio')
|
'seedRatioMode': self.conf('ratiomode')
|
||||||
}
|
}
|
||||||
|
|
||||||
if not filedata and data.get('type') == 'torrent':
|
if not filedata and data.get('type') == 'torrent':
|
||||||
@@ -58,15 +64,97 @@ class Transmission(Downloader):
|
|||||||
else:
|
else:
|
||||||
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||||
|
|
||||||
|
if not remote_torrent:
|
||||||
|
return False
|
||||||
|
|
||||||
# Change settings of added torrents
|
# Change settings of added torrents
|
||||||
if torrent_params:
|
elif torrent_params:
|
||||||
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||||
|
|
||||||
|
log.info('Torrent sent to Transmission successfully.')
|
||||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||||
except Exception, err:
|
except:
|
||||||
log.error('Failed to change settings for transfer: %s', err)
|
log.error('Failed to change settings for transfer: %s', traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
log.debug('Checking Transmission download status.')
|
||||||
|
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Go through Queue
|
||||||
|
try:
|
||||||
|
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
|
return_params = {
|
||||||
|
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
|
||||||
|
}
|
||||||
|
queue = trpc.get_alltorrents(return_params)
|
||||||
|
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed getting queue: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
# Get torrents status
|
||||||
|
# CouchPotato Status
|
||||||
|
#status = 'busy'
|
||||||
|
#status = 'failed'
|
||||||
|
#status = 'completed'
|
||||||
|
# Transmission Status
|
||||||
|
#status = 0 => "Torrent is stopped"
|
||||||
|
#status = 1 => "Queued to check files"
|
||||||
|
#status = 2 => "Checking files"
|
||||||
|
#status = 3 => "Queued to download"
|
||||||
|
#status = 4 => "Downloading"
|
||||||
|
#status = 4 => "Queued to seed"
|
||||||
|
#status = 6 => "Seeding"
|
||||||
|
#To do :
|
||||||
|
# add checking file
|
||||||
|
# manage no peer in a range time => fail
|
||||||
|
|
||||||
|
for item in queue['torrents']:
|
||||||
|
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished']))
|
||||||
|
|
||||||
|
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||||
|
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||||
|
return
|
||||||
|
|
||||||
|
if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'):
|
||||||
|
try:
|
||||||
|
trpc.stop_torrent(item['hashString'], {})
|
||||||
|
statuses.append({
|
||||||
|
'id': item['hashString'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': 'completed',
|
||||||
|
'original_status': item['status'],
|
||||||
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
|
'folder': os.path.join(item['downloadDir'], item['name']),
|
||||||
|
})
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err))
|
||||||
|
statuses.append({
|
||||||
|
'id': item['hashString'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': 'failed',
|
||||||
|
'original_status': item['status'],
|
||||||
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
statuses.append({
|
||||||
|
'id': item['hashString'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': 'busy',
|
||||||
|
'original_status': item['status'],
|
||||||
|
'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds??
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
class TransmissionRPC(object):
|
class TransmissionRPC(object):
|
||||||
|
|
||||||
@@ -97,6 +185,7 @@ class TransmissionRPC(object):
|
|||||||
try:
|
try:
|
||||||
open_request = urllib2.urlopen(request)
|
open_request = urllib2.urlopen(request)
|
||||||
response = json.loads(open_request.read())
|
response = json.loads(open_request.read())
|
||||||
|
log.debug('request: %s', json.dumps(ojson))
|
||||||
log.debug('response: %s', json.dumps(response))
|
log.debug('response: %s', json.dumps(response))
|
||||||
if response['result'] == 'success':
|
if response['result'] == 'success':
|
||||||
log.debug('Transmission action successfull')
|
log.debug('Transmission action successfull')
|
||||||
@@ -146,3 +235,18 @@ class TransmissionRPC(object):
|
|||||||
arguments['ids'] = torrent_id
|
arguments['ids'] = torrent_id
|
||||||
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
||||||
return self._request(post_data)
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def get_alltorrents(self, arguments):
|
||||||
|
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def stop_torrent(self, torrent_id, arguments):
|
||||||
|
arguments['ids'] = torrent_id
|
||||||
|
post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def remove_torrent(self, torrent_id, remove_local_data, arguments):
|
||||||
|
arguments['ids'] = torrent_id
|
||||||
|
arguments['delete-local-data'] = remove_local_data
|
||||||
|
post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from couchpotato.core.helpers.encoding import isInt, ss
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from multipartpost import MultipartPostHandler
|
from multipartpost import MultipartPostHandler
|
||||||
|
from datetime import timedelta
|
||||||
|
import os
|
||||||
import cookielib
|
import cookielib
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
@@ -104,6 +106,35 @@ class uTorrent(Downloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
statuses = StatusList(self)
|
statuses = StatusList(self)
|
||||||
|
download_folder = ''
|
||||||
|
settings_dict = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.utorrent_api.get_settings()
|
||||||
|
utorrent_settings = json.loads(data)
|
||||||
|
|
||||||
|
# Create settings dict
|
||||||
|
for item in utorrent_settings['settings']:
|
||||||
|
if item[1] == 0: # int
|
||||||
|
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
||||||
|
elif item[1] == 1: # bool
|
||||||
|
settings_dict[item[0]] = True if item[2] == 'true' else False
|
||||||
|
elif item[1] == 2: # string
|
||||||
|
settings_dict[item[0]] = item[2]
|
||||||
|
log.debug('uTorrent settings: %s', settings_dict)
|
||||||
|
|
||||||
|
# Get the download path from the uTorrent settings
|
||||||
|
if settings_dict['dir_completed_download_flag']:
|
||||||
|
download_folder = settings_dict['dir_completed_download']
|
||||||
|
elif settings_dict['dir_active_download_flag']:
|
||||||
|
download_folder = settings_dict['dir_active_download']
|
||||||
|
else:
|
||||||
|
log.info('No download folder set in uTorrent. Please set a download folder')
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to get settings from uTorrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
# Get torrents
|
# Get torrents
|
||||||
for item in queue.get('torrents', []):
|
for item in queue.get('torrents', []):
|
||||||
@@ -113,12 +144,18 @@ class uTorrent(Downloader):
|
|||||||
if item[21] == 'Finished' or item[21] == 'Seeding':
|
if item[21] == 'Finished' or item[21] == 'Seeding':
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
|
|
||||||
|
if settings_dict['dir_add_label']:
|
||||||
|
release_folder = os.path.join(download_folder, item[11], item[2])
|
||||||
|
else:
|
||||||
|
release_folder = os.path.join(download_folder, item[2])
|
||||||
|
|
||||||
statuses.append({
|
statuses.append({
|
||||||
'id': item[0],
|
'id': item[0],
|
||||||
'name': item[2],
|
'name': item[2],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item[1],
|
'original_status': item[1],
|
||||||
'timeleft': item[10],
|
'timeleft': str(timedelta(seconds = item[10])),
|
||||||
|
'folder': release_folder,
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
@@ -195,3 +232,7 @@ class uTorrentAPI(object):
|
|||||||
def get_status(self):
|
def get_status(self):
|
||||||
action = "list=1"
|
action = "list=1"
|
||||||
return self._request(action)
|
return self._request(action)
|
||||||
|
|
||||||
|
def get_settings(self):
|
||||||
|
action = "action=getsettings"
|
||||||
|
return self._request(action)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Notification(Provider):
|
|||||||
|
|
||||||
def _notify(self, *args, **kwargs):
|
def _notify(self, *args, **kwargs):
|
||||||
if self.isEnabled():
|
if self.isEnabled():
|
||||||
self.notify(*args, **kwargs)
|
return self.notify(*args, **kwargs)
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ class Automation(Plugin):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
|
addEvent('app.load', self.setCrons)
|
||||||
|
|
||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
addEvent('app.load', self.addMovies)
|
addEvent('app.load', self.addMovies)
|
||||||
|
|
||||||
|
addEvent('setting.save.automation.hour.after', self.setCrons)
|
||||||
|
|
||||||
|
def setCrons(self):
|
||||||
|
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
|
||||||
|
|
||||||
def addMovies(self):
|
def addMovies(self):
|
||||||
|
|
||||||
movies = fireEvent('automation.get_movies', merge = True)
|
movies = fireEvent('automation.get_movies', merge = True)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin
|
|||||||
from couchpotato.core.plugins.scanner.main import Scanner
|
from couchpotato.core.plugins.scanner.main import Scanner
|
||||||
from couchpotato.core.settings.model import FileType, File
|
from couchpotato.core.settings.model import FileType, File
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
from flask.helpers import send_file
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
import os.path
|
import os.path
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -81,11 +83,13 @@ class FileManager(Plugin):
|
|||||||
|
|
||||||
def showCacheFile(self, filename = ''):
|
def showCacheFile(self, filename = ''):
|
||||||
|
|
||||||
cache_dir = Env.get('cache_dir')
|
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
|
||||||
filename = os.path.basename(filename)
|
|
||||||
|
|
||||||
from flask.helpers import send_from_directory
|
if not os.path.isfile(file_path):
|
||||||
return send_from_directory(cache_dir, filename)
|
log.error('File "%s" not found', file_path)
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return send_file(file_path, conditional = True)
|
||||||
|
|
||||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
|
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class QualityPlugin(Plugin):
|
|||||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
|
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
|
||||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
|
|||||||
@@ -112,6 +112,15 @@ config = [{
|
|||||||
'label': 'Separator',
|
'label': 'Separator',
|
||||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'file_action',
|
||||||
|
'label': 'File Action',
|
||||||
|
'default': 'move',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink')],
|
||||||
|
'description': 'Define which kind of file operation you want to use. Before you start using <a href="http://en.wikipedia.org/wiki/Hard_link">hard links</a> or <a href="http://en.wikipedia.org/wiki/Sym_link">sym links</a>, PLEASE read about their possible drawbacks.',
|
||||||
|
'advanced': True,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'name': 'ntfs_permission',
|
'name': 'ntfs_permission',
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||||
from couchpotato.core.helpers.request import jsonified
|
from couchpotato.core.helpers.request import getParams, jsonified
|
||||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||||
getImdb
|
getImdb
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, File, Profile, Release
|
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
||||||
|
ReleaseInfo
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import errno
|
import errno
|
||||||
|
import linktastic.linktastic as linktastic
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -27,7 +29,12 @@ class Renamer(Plugin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
addApiView('renamer.scan', self.scanView, docs = {
|
addApiView('renamer.scan', self.scanView, docs = {
|
||||||
'desc': 'For the renamer to check for new files to rename',
|
'desc': 'For the renamer to check for new files to rename in a folder',
|
||||||
|
'params': {
|
||||||
|
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
|
||||||
|
'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'},
|
||||||
|
'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
addEvent('renamer.scan', self.scan)
|
addEvent('renamer.scan', self.scan)
|
||||||
@@ -35,22 +42,43 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
addEvent('app.load', self.scan)
|
addEvent('app.load', self.scan)
|
||||||
addEvent('app.load', self.checkSnatched)
|
addEvent('app.load', self.checkSnatched)
|
||||||
|
addEvent('app.load', self.setCrons)
|
||||||
|
|
||||||
if self.conf('run_every') > 0:
|
# Enable / disable interval
|
||||||
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
|
addEvent('setting.save.renamer.enabled.after', self.setCrons)
|
||||||
|
addEvent('setting.save.renamer.run_every.after', self.setCrons)
|
||||||
|
addEvent('setting.save.renamer.force_every.after', self.setCrons)
|
||||||
|
|
||||||
if self.conf('force_every') > 0:
|
def setCrons(self):
|
||||||
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
|
|
||||||
|
fireEvent('schedule.remove', 'renamer.check_snatched')
|
||||||
|
if self.isEnabled() and self.conf('run_every') > 0:
|
||||||
|
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True)
|
||||||
|
|
||||||
|
fireEvent('schedule.remove', 'renamer.check_snatched_forced')
|
||||||
|
if self.isEnabled() and self.conf('force_every') > 0:
|
||||||
|
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def scanView(self):
|
def scanView(self):
|
||||||
|
|
||||||
fireEventAsync('renamer.scan')
|
params = getParams()
|
||||||
|
movie_folder = params.get('movie_folder', None)
|
||||||
|
downloader = params.get('downloader', None)
|
||||||
|
download_id = params.get('download_id', None)
|
||||||
|
|
||||||
|
fireEventAsync('renamer.scan',
|
||||||
|
movie_folder = movie_folder,
|
||||||
|
downloader = downloader,
|
||||||
|
download_id = download_id
|
||||||
|
)
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True
|
'success': True
|
||||||
})
|
})
|
||||||
|
|
||||||
def scan(self):
|
def scan(self, movie_folder = None, downloader = None, download_id = None):
|
||||||
|
|
||||||
if self.isDisabled():
|
if self.isDisabled():
|
||||||
return
|
return
|
||||||
@@ -60,17 +88,61 @@ class Renamer(Plugin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Check to see if the "to" folder is inside the "from" folder.
|
# Check to see if the "to" folder is inside the "from" folder.
|
||||||
if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
|
if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
|
||||||
log.debug('"To" and "From" have to exist.')
|
log.error('"To" and "From" have to exist.')
|
||||||
return
|
return
|
||||||
elif self.conf('from') in self.conf('to'):
|
elif self.conf('from') in self.conf('to'):
|
||||||
log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
|
log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
|
||||||
return
|
return
|
||||||
|
elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]):
|
||||||
groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
|
log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.')
|
||||||
|
return
|
||||||
|
|
||||||
self.renaming_started = True
|
self.renaming_started = True
|
||||||
|
|
||||||
|
# make sure the movie folder name is included in the search
|
||||||
|
folder = None
|
||||||
|
movie_files = []
|
||||||
|
if movie_folder:
|
||||||
|
log.info('Scanning movie folder %s...', movie_folder)
|
||||||
|
movie_folder = movie_folder.rstrip(os.path.sep)
|
||||||
|
folder = os.path.dirname(movie_folder)
|
||||||
|
|
||||||
|
# Get all files from the specified folder
|
||||||
|
try:
|
||||||
|
for root, folders, names in os.walk(movie_folder):
|
||||||
|
movie_files.extend([os.path.join(root, name) for name in names])
|
||||||
|
except:
|
||||||
|
log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
# Get the release with the downloader ID that was downloded by the downloader
|
||||||
|
download_info = None
|
||||||
|
if download_id and downloader:
|
||||||
|
rls = None
|
||||||
|
|
||||||
|
rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all()
|
||||||
|
rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all()
|
||||||
|
|
||||||
|
for rlsnfo_dwnld in rlsnfo_dwnlds:
|
||||||
|
for rlsnfo_id in rlsnfo_ids:
|
||||||
|
if rlsnfo_id.release == rlsnfo_dwnld.release:
|
||||||
|
rls = rlsnfo_id.release
|
||||||
|
break
|
||||||
|
if rls: break
|
||||||
|
|
||||||
|
if rls:
|
||||||
|
download_info = {
|
||||||
|
'imdb_id': rls.movie.library.identifier,
|
||||||
|
'quality': rls.quality.identifier,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader))
|
||||||
|
|
||||||
|
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
|
||||||
|
files = movie_files, download_info = download_info, return_ignored = False, single = True)
|
||||||
|
|
||||||
destination = self.conf('to')
|
destination = self.conf('to')
|
||||||
folder_name = self.conf('folder_name')
|
folder_name = self.conf('folder_name')
|
||||||
file_name = self.conf('file_name')
|
file_name = self.conf('file_name')
|
||||||
@@ -84,8 +156,6 @@ class Renamer(Plugin):
|
|||||||
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
|
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
|
||||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||||
|
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
for group_identifier in groups:
|
for group_identifier in groups:
|
||||||
|
|
||||||
group = groups[group_identifier]
|
group = groups[group_identifier]
|
||||||
@@ -381,6 +451,9 @@ class Renamer(Plugin):
|
|||||||
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
||||||
self.tagDir(group, 'failed_rename')
|
self.tagDir(group, 'failed_rename')
|
||||||
|
|
||||||
|
if self.conf('file_action') != 'move':
|
||||||
|
self.tagDir(group, 'renamed already')
|
||||||
|
|
||||||
# Remove matching releases
|
# Remove matching releases
|
||||||
for release in remove_releases:
|
for release in remove_releases:
|
||||||
log.debug('Removing release %s', release.identifier)
|
log.debug('Removing release %s', release.identifier)
|
||||||
@@ -426,36 +499,35 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
return rename_files
|
return rename_files
|
||||||
|
|
||||||
|
# This adds a file to ignore / tag a release so it is ignored later
|
||||||
def tagDir(self, group, tag):
|
def tagDir(self, group, tag):
|
||||||
|
|
||||||
rename_files = {}
|
ignore_file = None
|
||||||
|
for movie_file in sorted(list(group['files']['movie'])):
|
||||||
|
ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0]
|
||||||
|
break
|
||||||
|
|
||||||
if group['dirname']:
|
text = """This file is from CouchPotato
|
||||||
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
|
It has marked this release as "%s"
|
||||||
else: # Add it to filename
|
This file hides the release from the renamer
|
||||||
for file_type in group['files']:
|
Remove it if you want it to be renamed (again, or at least let it try again)
|
||||||
for rename_me in group['files'][file_type]:
|
""" % tag
|
||||||
filename = os.path.basename(rename_me)
|
|
||||||
rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
|
|
||||||
|
|
||||||
for src in rename_files:
|
if ignore_file:
|
||||||
if rename_files[src]:
|
self.createFile(ignore_file, text)
|
||||||
dst = rename_files[src]
|
|
||||||
log.info('Renaming "%s" to "%s"', (src, dst))
|
|
||||||
|
|
||||||
# Create dir
|
|
||||||
self.makeDir(os.path.dirname(dst))
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.moveFile(src, dst)
|
|
||||||
except:
|
|
||||||
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
|
||||||
raise
|
|
||||||
|
|
||||||
def moveFile(self, old, dest):
|
def moveFile(self, old, dest):
|
||||||
dest = ss(dest)
|
dest = ss(dest)
|
||||||
try:
|
try:
|
||||||
shutil.move(old, dest)
|
if self.conf('file_action') == 'hardlink':
|
||||||
|
linktastic.link(old, dest)
|
||||||
|
elif self.conf('file_action') == 'symlink':
|
||||||
|
linktastic.symlink(old, dest)
|
||||||
|
elif self.conf('file_action') == 'copy':
|
||||||
|
shutil.copy(old, dest)
|
||||||
|
else:
|
||||||
|
shutil.move(old, dest)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.chmod(dest, Env.getPermission('file'))
|
os.chmod(dest, Env.getPermission('file'))
|
||||||
@@ -524,6 +596,7 @@ class Renamer(Plugin):
|
|||||||
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
||||||
|
|
||||||
def checkSnatched(self):
|
def checkSnatched(self):
|
||||||
|
|
||||||
if self.checking_snatched:
|
if self.checking_snatched:
|
||||||
log.debug('Already checking snatched')
|
log.debug('Already checking snatched')
|
||||||
|
|
||||||
@@ -597,7 +670,10 @@ class Renamer(Plugin):
|
|||||||
db.commit()
|
db.commit()
|
||||||
elif item['status'] == 'completed':
|
elif item['status'] == 'completed':
|
||||||
log.info('Download of %s completed!', item['name'])
|
log.info('Download of %s completed!', item['name'])
|
||||||
scan_required = True
|
if item['id'] and item['downloader'] and item['folder']:
|
||||||
|
fireEventAsync('renamer.scan', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id'])
|
||||||
|
else:
|
||||||
|
scan_required = True
|
||||||
|
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class Scanner(Plugin):
|
|||||||
addEvent('scanner.name_year', self.getReleaseNameYear)
|
addEvent('scanner.name_year', self.getReleaseNameYear)
|
||||||
addEvent('scanner.partnumber', self.getPartNumber)
|
addEvent('scanner.partnumber', self.getPartNumber)
|
||||||
|
|
||||||
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
|
def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
|
||||||
|
|
||||||
folder = ss(os.path.normpath(folder))
|
folder = ss(os.path.normpath(folder))
|
||||||
|
|
||||||
@@ -119,8 +119,7 @@ class Scanner(Plugin):
|
|||||||
try:
|
try:
|
||||||
files = []
|
files = []
|
||||||
for root, dirs, walk_files in os.walk(folder):
|
for root, dirs, walk_files in os.walk(folder):
|
||||||
for filename in walk_files:
|
files.extend(os.path.join(root, filename) for filename in walk_files)
|
||||||
files.append(os.path.join(root, filename))
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
||||||
else:
|
else:
|
||||||
@@ -178,17 +177,25 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
|
|
||||||
# Group files minus extension
|
# Group files minus extension
|
||||||
|
ignored_identifiers = []
|
||||||
for identifier, group in movie_files.iteritems():
|
for identifier, group in movie_files.iteritems():
|
||||||
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
|
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
|
||||||
|
|
||||||
log.debug('Grouping files: %s', identifier)
|
log.debug('Grouping files: %s', identifier)
|
||||||
|
|
||||||
|
has_ignored = 0
|
||||||
for file_path in group['unsorted_files']:
|
for file_path in group['unsorted_files']:
|
||||||
wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
|
ext = getExt(file_path)
|
||||||
|
wo_ext = file_path[:-(len(ext) + 1)]
|
||||||
found_files = set([i for i in leftovers if wo_ext in i])
|
found_files = set([i for i in leftovers if wo_ext in i])
|
||||||
group['unsorted_files'].extend(found_files)
|
group['unsorted_files'].extend(found_files)
|
||||||
leftovers = leftovers - found_files
|
leftovers = leftovers - found_files
|
||||||
|
|
||||||
|
has_ignored += 1 if ext == 'ignore' else 0
|
||||||
|
|
||||||
|
if has_ignored > 0:
|
||||||
|
ignored_identifiers.append(identifier)
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown():
|
||||||
break
|
break
|
||||||
@@ -314,6 +321,11 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
del movie_files
|
del movie_files
|
||||||
|
|
||||||
|
# Make sure only one movie was found if a download ID is provided
|
||||||
|
if download_info and not len(valid_files) == 1:
|
||||||
|
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
|
||||||
|
download_info = None
|
||||||
|
|
||||||
# Determine file types
|
# Determine file types
|
||||||
processed_movies = {}
|
processed_movies = {}
|
||||||
total_found = len(valid_files)
|
total_found = len(valid_files)
|
||||||
@@ -323,15 +335,17 @@ class Scanner(Plugin):
|
|||||||
except:
|
except:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if return_ignored is False and identifier in ignored_identifiers:
|
||||||
|
log.debug('Ignore file found, ignoring release: %s' % identifier)
|
||||||
|
continue
|
||||||
|
|
||||||
# Group extra (and easy) files first
|
# Group extra (and easy) files first
|
||||||
# images = self.getImages(group['unsorted_files'])
|
|
||||||
group['files'] = {
|
group['files'] = {
|
||||||
'movie_extra': self.getMovieExtras(group['unsorted_files']),
|
'movie_extra': self.getMovieExtras(group['unsorted_files']),
|
||||||
'subtitle': self.getSubtitles(group['unsorted_files']),
|
'subtitle': self.getSubtitles(group['unsorted_files']),
|
||||||
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
|
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
|
||||||
'nfo': self.getNfo(group['unsorted_files']),
|
'nfo': self.getNfo(group['unsorted_files']),
|
||||||
'trailer': self.getTrailers(group['unsorted_files']),
|
'trailer': self.getTrailers(group['unsorted_files']),
|
||||||
#'backdrop': images['backdrop'],
|
|
||||||
'leftover': set(group['unsorted_files']),
|
'leftover': set(group['unsorted_files']),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +360,7 @@ class Scanner(Plugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
log.debug('Getting metadata for %s', identifier)
|
log.debug('Getting metadata for %s', identifier)
|
||||||
group['meta_data'] = self.getMetaData(group, folder = folder)
|
group['meta_data'] = self.getMetaData(group, folder = folder, download_info = download_info)
|
||||||
|
|
||||||
# Subtitle meta
|
# Subtitle meta
|
||||||
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
||||||
@@ -376,7 +390,7 @@ class Scanner(Plugin):
|
|||||||
del group['unsorted_files']
|
del group['unsorted_files']
|
||||||
|
|
||||||
# Determine movie
|
# Determine movie
|
||||||
group['library'] = self.determineMovie(group)
|
group['library'] = self.determineMovie(group, download_info = download_info)
|
||||||
if not group['library']:
|
if not group['library']:
|
||||||
log.error('Unable to determine movie: %s', group['identifiers'])
|
log.error('Unable to determine movie: %s', group['identifiers'])
|
||||||
else:
|
else:
|
||||||
@@ -401,7 +415,7 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
return processed_movies
|
return processed_movies
|
||||||
|
|
||||||
def getMetaData(self, group, folder = ''):
|
def getMetaData(self, group, folder = '', download_info = None):
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
files = list(group['files']['movie'])
|
files = list(group['files']['movie'])
|
||||||
@@ -423,9 +437,13 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
if data.get('audio'): break
|
if data.get('audio'): break
|
||||||
|
|
||||||
|
# Use the quality guess first, if that failes use the quality we wanted to download
|
||||||
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
||||||
if not data['quality']:
|
if not data['quality']:
|
||||||
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
if download_info and download_info.get('quality'):
|
||||||
|
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
|
||||||
|
else:
|
||||||
|
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
||||||
|
|
||||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
|
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
|
||||||
|
|
||||||
@@ -501,17 +519,22 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
return detected_languages
|
return detected_languages
|
||||||
|
|
||||||
def determineMovie(self, group):
|
def determineMovie(self, group, download_info = None):
|
||||||
imdb_id = None
|
|
||||||
|
# Get imdb id from downloader
|
||||||
|
imdb_id = download_info and download_info.get('imdb_id')
|
||||||
|
if imdb_id:
|
||||||
|
log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('imdb_id'))
|
||||||
|
|
||||||
files = group['files']
|
files = group['files']
|
||||||
|
|
||||||
# Check for CP(imdb_id) string in the file paths
|
# Check for CP(imdb_id) string in the file paths
|
||||||
for cur_file in files['movie']:
|
if not imdb_id:
|
||||||
imdb_id = self.getCPImdb(cur_file)
|
for cur_file in files['movie']:
|
||||||
if imdb_id:
|
imdb_id = self.getCPImdb(cur_file)
|
||||||
log.debug('Found movie via CP tag: %s', cur_file)
|
if imdb_id:
|
||||||
break
|
log.debug('Found movie via CP tag: %s', cur_file)
|
||||||
|
break
|
||||||
|
|
||||||
# Check and see if nfo contains the imdb-id
|
# Check and see if nfo contains the imdb-id
|
||||||
if not imdb_id:
|
if not imdb_id:
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ class Searcher(Plugin):
|
|||||||
}"""},
|
}"""},
|
||||||
})
|
})
|
||||||
|
|
||||||
# Schedule cronjob
|
addEvent('app.load', self.setCrons)
|
||||||
|
addEvent('setting.save.searcher.cron_day.after', self.setCrons)
|
||||||
|
addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
|
||||||
|
addEvent('setting.save.searcher.cron_minute.after', self.setCrons)
|
||||||
|
|
||||||
|
def setCrons(self):
|
||||||
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||||
|
|
||||||
def allMoviesView(self):
|
def allMoviesView(self):
|
||||||
@@ -366,7 +371,7 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
return search_types
|
return search_types
|
||||||
|
|
||||||
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
|
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
|
||||||
|
|
||||||
imdb_results = kwargs.get('imdb_results', False)
|
imdb_results = kwargs.get('imdb_results', False)
|
||||||
retention = Env.setting('retention', section = 'nzb')
|
retention = Env.setting('retention', section = 'nzb')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from couchpotato.core.helpers.request import jsonified, getParams
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.movie.base import MovieProvider
|
from couchpotato.core.providers.movie.base import MovieProvider
|
||||||
from couchpotato.core.settings.model import Movie
|
from couchpotato.core.settings.model import Movie
|
||||||
|
from couchpotato.environment import Env
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -96,4 +97,5 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
'X-CP-Version': fireEvent('app.version', single = True),
|
'X-CP-Version': fireEvent('app.version', single = True),
|
||||||
'X-CP-API': self.api_version,
|
'X-CP-API': self.api_version,
|
||||||
'X-CP-Time': time.time(),
|
'X-CP-Time': time.time(),
|
||||||
|
'X-CP-Identifier': '+%s' % Env.setting('api_key', 'core')[:10], # Use first 10 as identifier, so we don't need to use IP address in api stats
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ config = [{
|
|||||||
'list': 'nzb_providers',
|
'list': 'nzb_providers',
|
||||||
'name': 'newznab',
|
'name': 'newznab',
|
||||||
'order': 10,
|
'order': 10,
|
||||||
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab providers</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> or <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>',
|
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
|
||||||
|
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
|
||||||
'wizard': True,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
@@ -23,18 +24,18 @@ config = [{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'use',
|
'name': 'use',
|
||||||
'default': '0,0,0,0'
|
'default': '0,0,0,0,0,0'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'host',
|
'name': 'host',
|
||||||
'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info',
|
'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
|
||||||
'description': 'The hostname of your newznab provider',
|
'description': 'The hostname of your newznab provider',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'extra_score',
|
'name': 'extra_score',
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'label': 'Extra Score',
|
'label': 'Extra Score',
|
||||||
'default': '0,0,0,0',
|
'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.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -189,6 +189,9 @@ class Settings(object):
|
|||||||
self.set(section, option, (new_value if new_value else value).encode('unicode_escape'))
|
self.set(section, option, (new_value if new_value else value).encode('unicode_escape'))
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# After save (for re-interval etc)
|
||||||
|
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
|
||||||
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
|
|
||||||
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
|
||||||
|
|
||||||
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
|
|
||||||
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
|
||||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
|
||||||
|
|
||||||
<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
|
|
||||||
<link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" />
|
|
||||||
|
|
||||||
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.addEvent('domready', function() {
|
|
||||||
new Uniform();
|
|
||||||
|
|
||||||
Api.setup({
|
|
||||||
'url': {{ url_for('api.index')|tojson|safe }},
|
|
||||||
'path_sep': {{ sep|tojson|safe }},
|
|
||||||
'is_remote': false
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document.body).set('data-api', window.location.protocol + '//' + window.location.host + Api.createUrl().replace('/default/', '/'));
|
|
||||||
|
|
||||||
// Catch errors
|
|
||||||
window.onerror = function(message, file, line){
|
|
||||||
|
|
||||||
p(message, file, line);
|
|
||||||
|
|
||||||
Api.request('logging.log', {
|
|
||||||
'data': {
|
|
||||||
'type': 'error',
|
|
||||||
'message': Browser.name + ' ' + Browser.version + ': \n' + message,
|
|
||||||
'page': window.location.href.replace(window.location.host, 'HOST'),
|
|
||||||
'file': file.replace(window.location.host, 'HOST'),
|
|
||||||
'line': line
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Quality.setup({
|
|
||||||
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
|
|
||||||
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
|
|
||||||
});
|
|
||||||
|
|
||||||
Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }});
|
|
||||||
|
|
||||||
File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }});
|
|
||||||
|
|
||||||
App.setup({
|
|
||||||
'base_url': {{ url_for('web.index')|tojson|safe }},
|
|
||||||
'args': {{ env.get('args')|tojson|safe }},
|
|
||||||
'options': {{ ('%s' % env.get('options'))|tojson|safe }},
|
|
||||||
'app_dir': {{ env.get('app_dir')|tojson|safe }},
|
|
||||||
'data_dir': {{ env.get('data_dir')|tojson|safe }},
|
|
||||||
'pid': {{ env.getPid()|tojson|safe }},
|
|
||||||
'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
{% if env.setting('show_wizard') %}
|
|
||||||
if(!window.location.href.contains('wizard'))
|
|
||||||
window.location = '{{ url_for('web.index') }}wizard/'
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<title>CouchPotato</title>
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
||||||
@@ -1 +1,78 @@
|
|||||||
{% extends "_desktop.html" %}
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
||||||
|
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
|
||||||
|
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
||||||
|
|
||||||
|
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
|
||||||
|
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
||||||
|
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
||||||
|
|
||||||
|
<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
|
||||||
|
<link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" />
|
||||||
|
|
||||||
|
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEvent('domready', function() {
|
||||||
|
new Uniform();
|
||||||
|
|
||||||
|
Api.setup({
|
||||||
|
'url': {{ url_for('api.index')|tojson|safe }},
|
||||||
|
'path_sep': {{ sep|tojson|safe }},
|
||||||
|
'is_remote': false
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document.body).set('data-api', window.location.protocol + '//' + window.location.host + Api.createUrl().replace('/default/', '/'));
|
||||||
|
|
||||||
|
// Catch errors
|
||||||
|
window.onerror = function(message, file, line){
|
||||||
|
|
||||||
|
p(message, file, line);
|
||||||
|
|
||||||
|
Api.request('logging.log', {
|
||||||
|
'data': {
|
||||||
|
'type': 'error',
|
||||||
|
'message': Browser.name + ' ' + Browser.version + ': \n' + message,
|
||||||
|
'page': window.location.href.replace(window.location.host, 'HOST'),
|
||||||
|
'file': file.replace(window.location.host, 'HOST'),
|
||||||
|
'line': line
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Quality.setup({
|
||||||
|
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
|
||||||
|
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
|
||||||
|
});
|
||||||
|
|
||||||
|
Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }});
|
||||||
|
|
||||||
|
File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }});
|
||||||
|
|
||||||
|
App.setup({
|
||||||
|
'base_url': {{ url_for('web.index')|tojson|safe }},
|
||||||
|
'args': {{ env.get('args')|tojson|safe }},
|
||||||
|
'options': {{ ('%s' % env.get('options'))|tojson|safe }},
|
||||||
|
'app_dir': {{ env.get('app_dir')|tojson|safe }},
|
||||||
|
'data_dir': {{ env.get('data_dir')|tojson|safe }},
|
||||||
|
'pid': {{ env.getPid()|tojson|safe }},
|
||||||
|
'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
{% if env.setting('show_wizard') %}
|
||||||
|
if(!window.location.href.contains('wizard'))
|
||||||
|
window.location = '{{ url_for('web.index') }}wizard/'
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<title>CouchPotato</title>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Linktastic
|
||||||
|
|
||||||
|
Linktastic is an extension of the os.link and os.symlink functionality provided
|
||||||
|
by the python language since version 2. Python only supports file linking on
|
||||||
|
*NIX-based systems, even though it is relatively simple to engineer a solution
|
||||||
|
to utilize NTFS's built-in linking functionality. Linktastic attempts to unify
|
||||||
|
linking on the windows platform with linking on *NIX-based systems.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
|
||||||
|
Linktastic is a single python module and can be imported as such. Examples:
|
||||||
|
|
||||||
|
# Hard linking src to dest
|
||||||
|
import linktastic
|
||||||
|
linktastic.link(src, dest)
|
||||||
|
|
||||||
|
# Symlinking src to dest
|
||||||
|
import linktastic
|
||||||
|
linktastic.symlink(src, dest)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Linktastic Module
|
||||||
|
# - A python2/3 compatible module that can create hardlinks/symlinks on windows-based systems
|
||||||
|
#
|
||||||
|
# Linktastic is distributed under the MIT License. The follow are the terms and conditions of using Linktastic.
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
# Copyright (c) 2012 Solipsis Development
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
# associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
# portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from subprocess import CalledProcessError
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# Prevent spaces from messing with us!
|
||||||
|
def _escape_param(param):
|
||||||
|
return '"%s"' % param
|
||||||
|
|
||||||
|
|
||||||
|
# Private function to create link on nt-based systems
|
||||||
|
def _link_windows(src, dest):
|
||||||
|
try:
|
||||||
|
subprocess.check_output(
|
||||||
|
'cmd /C mklink /H %s %s' % (_escape_param(dest), _escape_param(src)),
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
except CalledProcessError as err:
|
||||||
|
|
||||||
|
raise IOError(err.output.decode('utf-8'))
|
||||||
|
|
||||||
|
# TODO, find out what kind of messages Windows sends us from mklink
|
||||||
|
# print(stdout)
|
||||||
|
# assume if they ret-coded 0 we're good
|
||||||
|
|
||||||
|
|
||||||
|
def _symlink_windows(src, dest):
|
||||||
|
try:
|
||||||
|
subprocess.check_output(
|
||||||
|
'cmd /C mklink %s %s' % (_escape_param(dest), _escape_param(src)),
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
except CalledProcessError as err:
|
||||||
|
raise IOError(err.output.decode('utf-8'))
|
||||||
|
|
||||||
|
# TODO, find out what kind of messages Windows sends us from mklink
|
||||||
|
# print(stdout)
|
||||||
|
# assume if they ret-coded 0 we're good
|
||||||
|
|
||||||
|
|
||||||
|
# Create a hard link to src named as dest
|
||||||
|
# This version of link, unlike os.link, supports nt systems as well
|
||||||
|
def link(src, dest):
|
||||||
|
if os.name == 'nt':
|
||||||
|
_link_windows(src, dest)
|
||||||
|
else:
|
||||||
|
os.link(src, dest)
|
||||||
|
|
||||||
|
|
||||||
|
# Create a symlink to src named as dest, but don't fail if you're on nt
|
||||||
|
def symlink(src, dest):
|
||||||
|
if os.name == 'nt':
|
||||||
|
_symlink_windows(src, dest)
|
||||||
|
else:
|
||||||
|
os.symlink(src, dest)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Linktastic',
|
||||||
|
version='0.1.0',
|
||||||
|
author='Jon "Berkona" Monroe',
|
||||||
|
author_email='solipsis.dev@gmail.com',
|
||||||
|
py_modules=['linktastic'],
|
||||||
|
url="http://github.com/berkona/linktastic",
|
||||||
|
license='MIT License - See http://opensource.org/licenses/MIT for details',
|
||||||
|
description='Truly platform-independent file linking',
|
||||||
|
long_description=open('README.txt').read(),
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user