Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2013-04-05 23:57:54 +02:00
26 changed files with 579 additions and 213 deletions
+11 -41
View File
@@ -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)
} }
+9 -1
View File
@@ -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():
+1
View File
@@ -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
+5 -7
View File
@@ -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,
+117 -13
View File
@@ -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)
+42 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+6 -1
View File
@@ -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)
+8 -4
View File
@@ -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 = {}):
+1 -1
View File
@@ -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',
+113 -37
View File
@@ -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
+40 -17
View File
@@ -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:
+7 -2
View File
@@ -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.',
}, },
{ {
+3
View File
@@ -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,
}) })
-78
View File
@@ -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>
+78 -1
View File
@@ -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>
+19
View File
@@ -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)
+76
View File
@@ -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)
+13
View File
@@ -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(),
)