Until there is a more elegant solution to avoid unwanted white space trimming, this will let users disable that feature if it is not something they need.
1485 lines
69 KiB
Python
Executable File
1485 lines
69 KiB
Python
Executable File
import fnmatch
|
|
import os
|
|
import re
|
|
import shutil
|
|
import time
|
|
import traceback
|
|
|
|
from couchpotato import get_db
|
|
from couchpotato.api import addApiView
|
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
|
from couchpotato.core.helpers.encoding import toUnicode, ss, sp
|
|
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
|
getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder, \
|
|
getIdentifier, randomString, getFreeSpace, getSize
|
|
from couchpotato.core.logger import CPLog
|
|
from couchpotato.core.plugins.base import Plugin
|
|
from couchpotato.environment import Env
|
|
from unrar2 import RarFile
|
|
import six
|
|
from six.moves import filter
|
|
|
|
|
|
log = CPLog(__name__)
|
|
|
|
autoload = 'Renamer'
|
|
|
|
|
|
class Renamer(Plugin):
|
|
|
|
renaming_started = False
|
|
checking_snatched = False
|
|
|
|
def __init__(self):
|
|
addApiView('renamer.scan', self.scanView, docs = {
|
|
'desc': 'For the renamer to check for new files to rename in a folder',
|
|
'params': {
|
|
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
|
|
'to_folder': {'desc': 'Optional: The folder to move releases to. Leave empty for default folder.'},
|
|
'media_folder': {'desc': 'Optional: The folder of the media to scan. Keep empty for default renamer folder.'},
|
|
'files': {'desc': 'Optional: Provide the release files if more releases are in the same media_folder, delimited with a \'|\'. Note that no dedicated release folder is expected for releases with one file.'},
|
|
'base_folder': {'desc': 'Optional: The folder to find releases in. Leave empty for default folder.'},
|
|
'downloader': {'desc': 'Optional: The downloader the release has been downloaded with. \'download_id\' is required with this option.'},
|
|
'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in media_folder. \'downloader\' is required with this option.'},
|
|
'status': {'desc': 'Optional: The status of the release: \'completed\' (default) or \'seeding\''},
|
|
},
|
|
})
|
|
|
|
addApiView('renamer.progress', self.getProgress, docs = {
|
|
'desc': 'Get the progress of current renamer scan',
|
|
'return': {'type': 'object', 'example': """{
|
|
'progress': False || True,
|
|
}"""},
|
|
})
|
|
|
|
addEvent('renamer.scan', self.scan)
|
|
addEvent('renamer.check_snatched', self.checkSnatched)
|
|
|
|
addEvent('app.load', self.scan)
|
|
addEvent('app.load', self.setCrons)
|
|
|
|
# Enable / disable interval
|
|
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)
|
|
|
|
def setCrons(self):
|
|
|
|
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 getProgress(self, **kwargs):
|
|
return {
|
|
'progress': self.renaming_started
|
|
}
|
|
|
|
def scanView(self, **kwargs):
|
|
|
|
async = tryInt(kwargs.get('async', 0))
|
|
base_folder = kwargs.get('base_folder')
|
|
media_folder = sp(kwargs.get('media_folder'))
|
|
to_folder = kwargs.get('to_folder')
|
|
|
|
# Backwards compatibility, to be removed after a few versions :)
|
|
if not media_folder:
|
|
media_folder = sp(kwargs.get('movie_folder'))
|
|
|
|
downloader = kwargs.get('downloader')
|
|
download_id = kwargs.get('download_id')
|
|
files = [sp(filename) for filename in splitString(kwargs.get('files'), '|')]
|
|
status = kwargs.get('status', 'completed')
|
|
|
|
release_download = None
|
|
if not base_folder and media_folder:
|
|
release_download = {'folder': media_folder}
|
|
|
|
if download_id:
|
|
release_download.update({
|
|
'id': download_id,
|
|
'downloader': downloader,
|
|
'status': status,
|
|
'files': files
|
|
})
|
|
|
|
fire_handle = fireEvent if not async else fireEventAsync
|
|
fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download, to_folder = to_folder)
|
|
|
|
return {
|
|
'success': True
|
|
}
|
|
|
|
def scan(self, base_folder = None, release_download = None, to_folder = None):
|
|
if not release_download: release_download = {}
|
|
|
|
if self.isDisabled():
|
|
return
|
|
|
|
if self.renaming_started is True:
|
|
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
|
|
return
|
|
|
|
if not base_folder:
|
|
base_folder = sp(self.conf('from'))
|
|
|
|
from_folder = sp(self.conf('from'))
|
|
|
|
if not to_folder:
|
|
to_folder = sp(self.conf('to'))
|
|
|
|
# Get media folder to process
|
|
media_folder = sp(release_download.get('folder'))
|
|
|
|
# Get all folders that should not be processed
|
|
no_process = [to_folder]
|
|
cat_list = fireEvent('category.all', single = True) or []
|
|
no_process.extend([item['destination'] for item in cat_list])
|
|
|
|
# Check to see if the no_process folders are inside the "from" folder.
|
|
if not os.path.isdir(base_folder) or not os.path.isdir(to_folder):
|
|
log.error('Both the "To" and "From" folder have to exist.')
|
|
return
|
|
else:
|
|
for item in no_process:
|
|
if isSubFolder(item, base_folder):
|
|
log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder. "%s" in "%s"', (item, base_folder))
|
|
return
|
|
|
|
# Check to see if the no_process folders are inside the provided media_folder
|
|
if media_folder and not os.path.isdir(media_folder):
|
|
log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder)
|
|
|
|
# Update to the from folder
|
|
if len(release_download.get('files', [])) == 1:
|
|
new_media_folder = sp(from_folder)
|
|
else:
|
|
new_media_folder = sp(os.path.join(from_folder, os.path.basename(media_folder)))
|
|
|
|
if not os.path.isdir(new_media_folder):
|
|
log.error('The provided media folder %s does not exist and could also not be found in the \'from\' folder.', media_folder)
|
|
return
|
|
|
|
# Update the files
|
|
new_files = [os.path.join(new_media_folder, os.path.relpath(filename, media_folder)) for filename in release_download.get('files', [])]
|
|
if new_files and not os.path.isfile(new_files[0]):
|
|
log.error('The provided media folder %s does not exist and its files could also not be found in the \'from\' folder.', media_folder)
|
|
return
|
|
|
|
# Update release_download info to the from folder
|
|
log.debug('Release %s found in the \'from\' folder.', media_folder)
|
|
release_download['folder'] = new_media_folder
|
|
release_download['files'] = new_files
|
|
media_folder = new_media_folder
|
|
|
|
if media_folder:
|
|
for item in no_process:
|
|
if isSubFolder(item, media_folder):
|
|
log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder. "%s" in "%s"', (item, media_folder))
|
|
return
|
|
|
|
# Make sure a checkSnatched marked all downloads/seeds as such
|
|
if not release_download and self.conf('run_every') > 0:
|
|
self.checkSnatched(fire_scan = False)
|
|
|
|
self.renaming_started = True
|
|
|
|
# make sure the media folder name is included in the search
|
|
folder = None
|
|
files = []
|
|
if media_folder:
|
|
log.info('Scanning media folder %s...', media_folder)
|
|
folder = os.path.dirname(media_folder)
|
|
|
|
release_files = release_download.get('files', [])
|
|
if release_files:
|
|
files = release_files
|
|
|
|
# If there is only one file in the torrent, the downloader did not create a subfolder
|
|
if len(release_files) == 1:
|
|
folder = media_folder
|
|
else:
|
|
# Get all files from the specified folder
|
|
try:
|
|
for root, folders, names in os.walk(media_folder):
|
|
files.extend([sp(os.path.join(root, name)) for name in names])
|
|
except:
|
|
log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc()))
|
|
|
|
db = get_db()
|
|
|
|
# Extend the download info with info stored in the downloaded release
|
|
keep_original = self.moveTypeIsLinked()
|
|
is_torrent = False
|
|
if release_download:
|
|
release_download = self.extendReleaseDownload(release_download)
|
|
is_torrent = self.downloadIsTorrent(release_download)
|
|
keep_original = True if is_torrent and self.conf('file_action') not in ['move'] else keep_original
|
|
|
|
# Unpack any archives
|
|
extr_files = None
|
|
if self.conf('unrar'):
|
|
folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files,
|
|
cleanup = self.conf('cleanup') and not keep_original)
|
|
|
|
groups = fireEvent('scanner.scan', folder = folder if folder else base_folder,
|
|
files = files, release_download = release_download, return_ignored = False, single = True) or []
|
|
|
|
folder_name = self.conf('folder_name')
|
|
file_name = self.conf('file_name')
|
|
trailer_name = self.conf('trailer_name')
|
|
nfo_name = self.conf('nfo_name')
|
|
separator = self.conf('separator')
|
|
|
|
if len(file_name) == 0:
|
|
log.error('Please fill in the filename option under renamer settings. Forcing it on <original>.<ext> to keep the same name as source file.')
|
|
file_name = '<original>.<ext>'
|
|
|
|
cd_keys = ['<cd>','<cd_nr>', '<original>']
|
|
if not any(x in folder_name for x in cd_keys) and not any(x in file_name for x in cd_keys):
|
|
log.error('Missing `cd` or `cd_nr` in the renamer. This will cause multi-file releases of being renamed to the same file. '
|
|
'Please add it in the renamer settings. Force adding it for now.')
|
|
file_name = '%s %s' % ('<cd>', file_name)
|
|
|
|
# Tag release folder as failed_rename in case no groups were found. This prevents check_snatched from removing the release from the downloader.
|
|
if not groups and self.statusInfoComplete(release_download):
|
|
self.tagRelease(release_download = release_download, tag = 'failed_rename')
|
|
|
|
for group_identifier in groups:
|
|
|
|
group = groups[group_identifier]
|
|
group['release_download'] = None
|
|
rename_files = {}
|
|
remove_files = []
|
|
remove_releases = []
|
|
|
|
media_title = getTitle(group)
|
|
|
|
# Add _UNKNOWN_ if no library item is connected
|
|
if not group.get('media') or not media_title:
|
|
self.tagRelease(group = group, tag = 'unknown')
|
|
continue
|
|
# Rename the files using the library data
|
|
else:
|
|
|
|
# Media not in library, add it first
|
|
if not group['media'].get('_id'):
|
|
group['media'] = fireEvent('movie.add', params = {
|
|
'identifier': group['identifier'],
|
|
'profile_id': None
|
|
}, search_after = False, status = 'done', single = True)
|
|
else:
|
|
group['media'] = fireEvent('movie.update', media_id = group['media'].get('_id'), single = True)
|
|
|
|
if not group['media'] or not group['media'].get('_id'):
|
|
log.error('Could not rename, no library item to work with: %s', group_identifier)
|
|
continue
|
|
|
|
media = group['media']
|
|
media_title = getTitle(media)
|
|
|
|
# Overwrite destination when set in category
|
|
destination = to_folder
|
|
category_label = ''
|
|
|
|
if media.get('category_id') and media.get('category_id') != '-1':
|
|
try:
|
|
category = db.get('id', media['category_id'])
|
|
category_label = category['label']
|
|
|
|
if category['destination'] and len(category['destination']) > 0 and category['destination'] != 'None':
|
|
destination = sp(category['destination'])
|
|
log.debug('Setting category destination for "%s": %s' % (media_title, destination))
|
|
else:
|
|
log.debug('No category destination found for "%s"' % media_title)
|
|
except:
|
|
log.error('Failed getting category label: %s', traceback.format_exc())
|
|
|
|
|
|
# Find subtitle for renaming
|
|
group['before_rename'] = []
|
|
fireEvent('renamer.before', group)
|
|
|
|
# Add extracted files to the before_rename list
|
|
if extr_files:
|
|
group['before_rename'].extend(extr_files)
|
|
|
|
# Remove weird chars from movie name
|
|
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', media_title)
|
|
|
|
# Put 'The' at the end
|
|
name_the = movie_name
|
|
for prefix in ['the ', 'an ', 'a ']:
|
|
if prefix == movie_name[:len(prefix)].lower():
|
|
name_the = movie_name[len(prefix):] + ', ' + prefix.strip().capitalize()
|
|
break
|
|
|
|
replacements = {
|
|
'ext': 'mkv',
|
|
'namethe': name_the.strip(),
|
|
'thename': movie_name.strip(),
|
|
'year': media['info']['year'],
|
|
'first': name_the[0].upper(),
|
|
'quality': group['meta_data']['quality']['label'],
|
|
'quality_type': group['meta_data']['quality_type'],
|
|
'video': group['meta_data'].get('video'),
|
|
'audio': group['meta_data'].get('audio'),
|
|
'group': group['meta_data']['group'],
|
|
'source': group['meta_data']['source'],
|
|
'resolution_width': group['meta_data'].get('resolution_width'),
|
|
'resolution_height': group['meta_data'].get('resolution_height'),
|
|
'audio_channels': group['meta_data'].get('audio_channels'),
|
|
'imdb_id': group['identifier'],
|
|
'cd': '',
|
|
'cd_nr': '',
|
|
'mpaa': media['info'].get('mpaa', ''),
|
|
'mpaa_only': media['info'].get('mpaa', ''),
|
|
'category': category_label,
|
|
'3d': '3D' if group['meta_data']['quality'].get('is_3d', 0) else '',
|
|
'3d_type': group['meta_data'].get('3d_type'),
|
|
}
|
|
|
|
if replacements['mpaa_only'] not in ('G', 'PG', 'PG-13', 'R', 'NC-17'):
|
|
replacements['mpaa_only'] = 'Not Rated'
|
|
|
|
for file_type in group['files']:
|
|
|
|
# Move nfo depending on settings
|
|
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
|
log.debug('Skipping, renaming of %s disabled', file_type)
|
|
for current_file in group['files'][file_type]:
|
|
if self.conf('cleanup') and (not keep_original or self.fileIsAdded(current_file, group)):
|
|
remove_files.append(current_file)
|
|
continue
|
|
|
|
# Subtitle extra
|
|
if file_type is 'subtitle_extra':
|
|
continue
|
|
|
|
# Move other files
|
|
multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
|
|
cd = 1 if multiple else 0
|
|
|
|
for current_file in sorted(list(group['files'][file_type])):
|
|
current_file = sp(current_file)
|
|
|
|
# Original filename
|
|
replacements['original'] = os.path.splitext(os.path.basename(current_file))[0]
|
|
replacements['original_folder'] = fireEvent('scanner.remove_cptag', group['dirname'], single = True)
|
|
|
|
if not replacements['original_folder'] or len(replacements['original_folder']) == 0:
|
|
replacements['original_folder'] = replacements['original']
|
|
|
|
# Extension
|
|
replacements['ext'] = getExt(current_file)
|
|
|
|
# cd #
|
|
replacements['cd'] = ' cd%d' % cd if multiple else ''
|
|
replacements['cd_nr'] = cd if multiple else ''
|
|
|
|
# Naming
|
|
final_folder_name = self.doReplace(folder_name, replacements, folder = True)
|
|
final_file_name = self.doReplace(file_name, replacements)
|
|
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
|
|
|
# Meta naming
|
|
if file_type is 'trailer':
|
|
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
|
|
elif file_type is 'nfo':
|
|
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
|
|
|
|
# Move DVD files (no structure renaming)
|
|
if group['is_dvd'] and file_type is 'movie':
|
|
found = False
|
|
for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']:
|
|
has_string = current_file.lower().find(os.path.sep + top_dir + os.path.sep)
|
|
if has_string >= 0:
|
|
structure_dir = current_file[has_string:].lstrip(os.path.sep)
|
|
rename_files[current_file] = os.path.join(destination, final_folder_name, structure_dir)
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
log.error('Could not determine dvd structure for: %s', current_file)
|
|
|
|
# Do rename others
|
|
else:
|
|
if file_type is 'leftover':
|
|
if self.conf('move_leftover'):
|
|
rename_files[current_file] = os.path.join(destination, final_folder_name, os.path.basename(current_file))
|
|
elif file_type not in ['subtitle']:
|
|
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
|
|
|
# Check for extra subtitle files
|
|
if file_type is 'subtitle':
|
|
|
|
remove_multiple = False
|
|
if len(group['files']['movie']) == 1:
|
|
remove_multiple = True
|
|
|
|
sub_langs = group['subtitle_language'].get(current_file, [])
|
|
|
|
# rename subtitles with or without language
|
|
sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
|
|
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
|
|
|
rename_extras = self.getRenameExtras(
|
|
extra_type = 'subtitle_extra',
|
|
replacements = replacements,
|
|
folder_name = folder_name,
|
|
file_name = file_name,
|
|
destination = destination,
|
|
group = group,
|
|
current_file = current_file,
|
|
remove_multiple = remove_multiple,
|
|
)
|
|
|
|
# Don't add language if multiple languages in 1 subtitle file
|
|
if len(sub_langs) == 1:
|
|
sub_suffix = '%s.%s' % (sub_langs[0], replacements['ext'])
|
|
|
|
# Don't add language to subtitle file it it's already there
|
|
if not sub_name.endswith(sub_suffix):
|
|
sub_name = sub_name.replace(replacements['ext'], sub_suffix)
|
|
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
|
|
|
rename_files = mergeDicts(rename_files, rename_extras)
|
|
|
|
# Filename without cd etc
|
|
elif file_type is 'movie':
|
|
rename_extras = self.getRenameExtras(
|
|
extra_type = 'movie_extra',
|
|
replacements = replacements,
|
|
folder_name = folder_name,
|
|
file_name = file_name,
|
|
destination = destination,
|
|
group = group,
|
|
current_file = current_file
|
|
)
|
|
rename_files = mergeDicts(rename_files, rename_extras)
|
|
|
|
group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
|
|
group['destination_dir'] = os.path.join(destination, final_folder_name)
|
|
|
|
if multiple:
|
|
cd += 1
|
|
|
|
# Before renaming, remove the lower quality files
|
|
remove_leftovers = True
|
|
|
|
# Get media quality profile
|
|
profile = None
|
|
if media.get('profile_id'):
|
|
try:
|
|
profile = db.get('id', media['profile_id'])
|
|
except:
|
|
# Set profile to None as it does not exist anymore
|
|
mdia = db.get('id', media['_id'])
|
|
mdia['profile_id'] = None
|
|
db.update(mdia)
|
|
log.error('Error getting quality profile for %s: %s', (media_title, traceback.format_exc()))
|
|
else:
|
|
log.debug('Media has no quality profile: %s', media_title)
|
|
|
|
# Mark media for dashboard
|
|
mark_as_recent = False
|
|
|
|
# Go over current movie releases
|
|
for release in fireEvent('release.for_media', media['_id'], single = True):
|
|
|
|
# When a release already exists
|
|
if release.get('status') == 'done':
|
|
|
|
# This is where CP removes older, lesser quality releases or releases that are not wanted anymore
|
|
is_higher = fireEvent('quality.ishigher', \
|
|
group['meta_data']['quality'], {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, single = True)
|
|
|
|
if is_higher == 'higher':
|
|
log.info('Removing lesser or not wanted quality %s for %s.', (media_title, release.get('quality')))
|
|
for file_type in release.get('files', {}):
|
|
for release_file in release['files'][file_type]:
|
|
remove_files.append(release_file)
|
|
remove_releases.append(release)
|
|
|
|
# Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
|
|
elif is_higher == 'equal':
|
|
log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (media_title, release.get('quality')))
|
|
for file_type in release.get('files', {}):
|
|
for release_file in release['files'][file_type]:
|
|
remove_files.append(release_file)
|
|
remove_releases.append(release)
|
|
|
|
# Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
|
|
else:
|
|
log.info('Better quality release already exists for %s, with quality %s', (media_title, release.get('quality')))
|
|
|
|
# Add exists tag to the .ignore file
|
|
self.tagRelease(group = group, tag = 'exists')
|
|
|
|
# Notify on rename fail
|
|
download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (media_title, group['meta_data']['quality']['label'], release.get('quality'))
|
|
fireEvent('movie.renaming.canceled', message = download_message, data = group)
|
|
remove_leftovers = False
|
|
|
|
break
|
|
|
|
elif release.get('status') in ['snatched', 'seeding']:
|
|
if release_download and release_download.get('release_id'):
|
|
if release_download['release_id'] == release['_id']:
|
|
if release_download['status'] == 'completed':
|
|
# Set the release to downloaded
|
|
fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
|
|
group['release_download'] = release_download
|
|
mark_as_recent = True
|
|
elif release_download['status'] == 'seeding':
|
|
# Set the release to seeding
|
|
fireEvent('release.update_status', release['_id'], status = 'seeding', single = True)
|
|
mark_as_recent = True
|
|
|
|
elif release.get('quality') == group['meta_data']['quality']['identifier']:
|
|
# Set the release to downloaded
|
|
fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
|
|
group['release_download'] = release_download
|
|
mark_as_recent = True
|
|
|
|
# Mark media for dashboard
|
|
if mark_as_recent:
|
|
fireEvent('media.tag', group['media'].get('_id'), 'recent', update_edited = True, single = True)
|
|
|
|
# Remove leftover files
|
|
if not remove_leftovers: # Don't remove anything
|
|
continue
|
|
|
|
log.debug('Removing leftover files')
|
|
for current_file in group['files']['leftover']:
|
|
if self.conf('cleanup') and not self.conf('move_leftover') and \
|
|
(not keep_original or self.fileIsAdded(current_file, group)):
|
|
remove_files.append(current_file)
|
|
|
|
if self.conf('check_space'):
|
|
total_space, available_space = getFreeSpace(destination)
|
|
renaming_size = getSize(rename_files.keys())
|
|
if renaming_size > available_space:
|
|
log.error('Not enough space left, need %s MB but only %s MB available', (renaming_size, available_space))
|
|
self.tagRelease(group = group, tag = 'not_enough_space')
|
|
continue
|
|
|
|
# Remove files
|
|
delete_folders = []
|
|
for src in remove_files:
|
|
|
|
if rename_files.get(src):
|
|
log.debug('Not removing file that will be renamed: %s', src)
|
|
continue
|
|
|
|
log.info('Removing "%s"', src)
|
|
try:
|
|
src = sp(src)
|
|
if os.path.isfile(src):
|
|
os.remove(src)
|
|
|
|
parent_dir = os.path.dirname(src)
|
|
if parent_dir not in delete_folders and os.path.isdir(parent_dir) and \
|
|
not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \
|
|
isSubFolder(parent_dir, base_folder):
|
|
|
|
delete_folders.append(parent_dir)
|
|
|
|
except:
|
|
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
|
|
self.tagRelease(group = group, tag = 'failed_remove')
|
|
|
|
# Delete leftover folder from older releases
|
|
delete_folders = sorted(delete_folders, key = len, reverse = True)
|
|
for delete_folder in delete_folders:
|
|
try:
|
|
self.deleteEmptyFolder(delete_folder, show_error = False)
|
|
except Exception as e:
|
|
log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
|
|
|
|
# Rename all files marked
|
|
group['renamed_files'] = []
|
|
failed_rename = False
|
|
for src in rename_files:
|
|
if rename_files[src]:
|
|
dst = rename_files[src]
|
|
|
|
if dst in group['renamed_files']:
|
|
log.error('File "%s" already renamed once, adding random string at the end to prevent data loss', dst)
|
|
dst = '%s.random-%s' % (dst, randomString())
|
|
|
|
# Create dir
|
|
self.makeDir(os.path.dirname(dst))
|
|
|
|
try:
|
|
self.moveFile(src, dst, use_default = not is_torrent or self.fileIsAdded(src, group))
|
|
group['renamed_files'].append(dst)
|
|
except:
|
|
log.error('Failed renaming the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
|
failed_rename = True
|
|
break
|
|
|
|
# If renaming failed tag the release folder as failed and continue with next group. Note that all old files have already been deleted.
|
|
if failed_rename:
|
|
self.tagRelease(group = group, tag = 'failed_rename')
|
|
continue
|
|
# If renaming succeeded, make sure it is not tagged as failed (scanner didn't return a group, but a download_ID was provided in an earlier attempt)
|
|
else:
|
|
self.untagRelease(group = group, tag = 'failed_rename')
|
|
|
|
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
|
|
if self.movieInFromFolder(media_folder) and keep_original:
|
|
self.tagRelease(group = group, tag = 'renamed_already')
|
|
|
|
# Remove matching releases
|
|
for release in remove_releases:
|
|
log.debug('Removing release %s', release.get('identifier'))
|
|
try:
|
|
db.delete(release)
|
|
except:
|
|
log.error('Failed removing %s: %s', (release, traceback.format_exc()))
|
|
|
|
if group['dirname'] and group['parentdir'] and not keep_original:
|
|
if media_folder:
|
|
# Delete the movie folder
|
|
group_folder = media_folder
|
|
else:
|
|
# Delete the first empty subfolder in the tree relative to the 'from' folder
|
|
group_folder = sp(os.path.join(base_folder, os.path.relpath(group['parentdir'], base_folder).split(os.path.sep)[0]))
|
|
|
|
try:
|
|
if self.conf('cleanup') or self.conf('move_leftover'):
|
|
log.info('Deleting folder: %s', group_folder)
|
|
self.deleteEmptyFolder(group_folder)
|
|
except:
|
|
log.error('Failed removing %s: %s', (group_folder, traceback.format_exc()))
|
|
|
|
# Notify on download, search for trailers etc
|
|
download_message = 'Downloaded %s (%s%s)' % (media_title, replacements['quality'], (' ' + replacements['3d']) if replacements['3d'] else '')
|
|
try:
|
|
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
|
|
except:
|
|
log.error('Failed firing (some) of the renamer.after events: %s', traceback.format_exc())
|
|
|
|
# Break if CP wants to shut down
|
|
if self.shuttingDown():
|
|
break
|
|
|
|
self.renaming_started = False
|
|
|
|
def getRenameExtras(self, extra_type = '', replacements = None, folder_name = '', file_name = '', destination = '', group = None, current_file = '', remove_multiple = False):
|
|
if not group: group = {}
|
|
if not replacements: replacements = {}
|
|
|
|
replacements = replacements.copy()
|
|
rename_files = {}
|
|
|
|
def test(s):
|
|
return current_file[:-len(replacements['ext'])] in sp(s)
|
|
|
|
for extra in set(filter(test, group['files'][extra_type])):
|
|
replacements['ext'] = getExt(extra)
|
|
|
|
final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple, folder = True)
|
|
final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
|
|
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
|
|
|
|
return rename_files
|
|
|
|
# This adds a file to ignore / tag a release so it is ignored later
|
|
def tagRelease(self, tag, group = None, release_download = None):
|
|
if not tag:
|
|
return
|
|
|
|
text = """This file is from CouchPotato
|
|
It has marked this release as "%s"
|
|
This file hides the release from the renamer
|
|
Remove it if you want it to be renamed (again, or at least let it try again)
|
|
""" % tag
|
|
|
|
tag_files = []
|
|
|
|
# Tag movie files if they are known
|
|
if isinstance(group, dict):
|
|
tag_files = [sorted(list(group['files']['movie']))[0]]
|
|
|
|
elif isinstance(release_download, dict):
|
|
# Tag download_files if they are known
|
|
if release_download.get('files', []):
|
|
tag_files = [filename for filename in release_download.get('files', []) if os.path.exists(filename)]
|
|
|
|
# Tag all files in release folder
|
|
elif release_download['folder']:
|
|
for root, folders, names in os.walk(sp(release_download['folder'])):
|
|
tag_files.extend([os.path.join(root, name) for name in names])
|
|
|
|
for filename in tag_files:
|
|
|
|
# Don't tag .ignore files
|
|
if os.path.splitext(filename)[1] == '.ignore':
|
|
continue
|
|
|
|
tag_filename = '%s.%s.ignore' % (os.path.splitext(filename)[0], tag)
|
|
if not os.path.isfile(tag_filename):
|
|
self.createFile(tag_filename, text)
|
|
|
|
def untagRelease(self, group = None, release_download = None, tag = ''):
|
|
if not release_download:
|
|
return
|
|
|
|
tag_files = []
|
|
folder = None
|
|
|
|
# Tag movie files if they are known
|
|
if isinstance(group, dict):
|
|
tag_files = [sorted(list(group['files']['movie']))[0]]
|
|
|
|
folder = sp(group['parentdir'])
|
|
if not group.get('dirname') or not os.path.isdir(folder):
|
|
return False
|
|
|
|
elif isinstance(release_download, dict):
|
|
|
|
folder = sp(release_download['folder'])
|
|
if not os.path.isdir(folder):
|
|
return False
|
|
|
|
# Untag download_files if they are known
|
|
if release_download.get('files'):
|
|
tag_files = release_download.get('files', [])
|
|
|
|
# Untag all files in release folder
|
|
else:
|
|
for root, folders, names in os.walk(folder):
|
|
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
|
|
|
if not folder:
|
|
return False
|
|
|
|
# Find all .ignore files in folder
|
|
ignore_files = []
|
|
for root, dirnames, filenames in os.walk(folder):
|
|
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
|
|
|
|
# Match all found ignore files with the tag_files and delete if found
|
|
for tag_file in tag_files:
|
|
ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
|
|
for filename in ignore_file:
|
|
try:
|
|
os.remove(filename)
|
|
except:
|
|
log.debug('Unable to remove ignore file: %s. Error: %s.' % (filename, traceback.format_exc()))
|
|
|
|
def hastagRelease(self, release_download, tag = ''):
|
|
if not release_download:
|
|
return False
|
|
|
|
folder = sp(release_download['folder'])
|
|
if not os.path.isdir(folder):
|
|
return False
|
|
|
|
tag_files = []
|
|
ignore_files = []
|
|
|
|
# Find tag on download_files if they are known
|
|
if release_download.get('files'):
|
|
tag_files = release_download.get('files', [])
|
|
|
|
# Find tag on all files in release folder
|
|
else:
|
|
for root, folders, names in os.walk(folder):
|
|
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
|
|
|
# Find all .ignore files in folder
|
|
for root, dirnames, filenames in os.walk(folder):
|
|
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
|
|
|
|
# Match all found ignore files with the tag_files and return True found
|
|
for tag_file in tag_files:
|
|
ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
|
|
if ignore_file:
|
|
return True
|
|
|
|
return False
|
|
|
|
def moveFile(self, old, dest, use_default = False):
|
|
dest = sp(dest)
|
|
try:
|
|
|
|
if os.path.exists(dest) and os.path.isfile(dest):
|
|
raise Exception('Destination "%s" already exists' % dest)
|
|
|
|
move_type = self.conf('file_action')
|
|
if use_default:
|
|
move_type = self.conf('default_file_action')
|
|
|
|
if move_type not in ['copy', 'link']:
|
|
try:
|
|
log.info('Moving "%s" to "%s"', (old, dest))
|
|
shutil.move(old, dest)
|
|
except:
|
|
exists = os.path.exists(dest)
|
|
if exists and os.path.getsize(old) == os.path.getsize(dest):
|
|
log.error('Successfully moved file "%s", but something went wrong: %s', (dest, traceback.format_exc()))
|
|
os.unlink(old)
|
|
else:
|
|
# remove faultly copied file
|
|
if exists:
|
|
os.unlink(dest)
|
|
raise
|
|
elif move_type == 'copy':
|
|
log.info('Copying "%s" to "%s"', (old, dest))
|
|
shutil.copy(old, dest)
|
|
else:
|
|
log.info('Linking "%s" to "%s"', (old, dest))
|
|
# First try to hardlink
|
|
try:
|
|
log.debug('Hardlinking file "%s" to "%s"...', (old, dest))
|
|
link(old, dest)
|
|
except:
|
|
# Try to simlink next
|
|
log.debug('Couldn\'t hardlink file "%s" to "%s". Symlinking instead. Error: %s.', (old, dest, traceback.format_exc()))
|
|
shutil.copy(old, dest)
|
|
try:
|
|
old_link = '%s.link' % sp(old)
|
|
symlink(dest, old_link)
|
|
os.unlink(old)
|
|
os.rename(old_link, old)
|
|
except:
|
|
log.error('Couldn\'t symlink file "%s" to "%s". Copied instead. Error: %s. ', (old, dest, traceback.format_exc()))
|
|
|
|
try:
|
|
os.chmod(dest, Env.getPermission('file'))
|
|
if os.name == 'nt' and self.conf('ntfs_permission'):
|
|
os.popen('icacls "' + dest + '"* /reset /T')
|
|
except:
|
|
log.debug('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
|
|
except:
|
|
log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
|
|
raise
|
|
|
|
return True
|
|
|
|
def doReplace(self, string, replacements, remove_multiple = False, folder = False):
|
|
"""
|
|
replace confignames with the real thing
|
|
"""
|
|
|
|
replacements = replacements.copy()
|
|
if remove_multiple:
|
|
replacements['cd'] = ''
|
|
replacements['cd_nr'] = ''
|
|
|
|
replaced = toUnicode(string)
|
|
for x, r in replacements.items():
|
|
if x in ['thename', 'namethe']:
|
|
continue
|
|
if r is not None:
|
|
replaced = replaced.replace(six.u('<%s>') % toUnicode(x), toUnicode(r))
|
|
else:
|
|
#If information is not available, we don't want the tag in the filename
|
|
replaced = replaced.replace('<' + x + '>', '')
|
|
|
|
if self.conf('replace_doubles'):
|
|
replaced = self.replaceDoubles(replaced.lstrip('. '))
|
|
|
|
for x, r in replacements.items():
|
|
if x in ['thename', 'namethe']:
|
|
replaced = replaced.replace(six.u('<%s>') % toUnicode(x), toUnicode(r))
|
|
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
|
|
|
|
sep = self.conf('foldersep') if folder else self.conf('separator')
|
|
return ss(replaced.replace(' ', ' ' if not sep else sep))
|
|
|
|
def replaceDoubles(self, string):
|
|
|
|
replaces = [
|
|
('\.+', '.'), ('_+', '_'), ('-+', '-'), ('\s+', ' '), (' \\\\', '\\\\'), (' /', '/'),
|
|
('(\s\.)+', '.'), ('(-\.)+', '.'), ('(\s-)+', '-'),
|
|
]
|
|
|
|
for r in replaces:
|
|
reg, replace_with = r
|
|
string = re.sub(reg, replace_with, string)
|
|
|
|
string = string.rstrip(',_-/\\ ')
|
|
|
|
return string
|
|
|
|
def checkSnatched(self, fire_scan = True):
|
|
|
|
if self.checking_snatched:
|
|
log.debug('Already checking snatched')
|
|
return False
|
|
|
|
self.checking_snatched = True
|
|
|
|
try:
|
|
db = get_db()
|
|
|
|
rels = list(fireEvent('release.with_status', ['snatched', 'seeding', 'missing'], single = True))
|
|
|
|
if not rels:
|
|
#No releases found that need status checking
|
|
self.checking_snatched = False
|
|
return True
|
|
|
|
# Collect all download information with the download IDs from the releases
|
|
download_ids = []
|
|
no_status_support = []
|
|
try:
|
|
for rel in rels:
|
|
if not rel.get('download_info'): continue
|
|
|
|
if rel['download_info'].get('id') and rel['download_info'].get('downloader'):
|
|
download_ids.append(rel['download_info'])
|
|
|
|
ds = rel['download_info'].get('status_support')
|
|
if ds is False or ds == 'False':
|
|
no_status_support.append(ss(rel['download_info'].get('downloader')))
|
|
except:
|
|
log.error('Error getting download IDs from database')
|
|
self.checking_snatched = False
|
|
return False
|
|
|
|
release_downloads = fireEvent('download.status', download_ids, merge = True) if download_ids else []
|
|
|
|
if len(no_status_support) > 0:
|
|
log.debug('Download status functionality is not implemented for one of the active downloaders: %s', list(set(no_status_support)))
|
|
|
|
if not release_downloads:
|
|
if fire_scan:
|
|
self.scan()
|
|
|
|
self.checking_snatched = False
|
|
return True
|
|
|
|
scan_releases = []
|
|
scan_required = False
|
|
|
|
log.debug('Checking status snatched releases...')
|
|
|
|
try:
|
|
for rel in rels:
|
|
movie_dict = db.get('id', rel.get('media_id'))
|
|
download_info = rel.get('download_info')
|
|
|
|
if not isinstance(download_info, dict):
|
|
log.error('Faulty release found without any info, ignoring.')
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True)
|
|
continue
|
|
|
|
# Check if download ID is available
|
|
if not download_info.get('id') or not download_info.get('downloader'):
|
|
log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (download_info.get('downloader', 'unknown'), rel['info']['name']))
|
|
scan_required = True
|
|
|
|
# Continue with next release
|
|
continue
|
|
|
|
# Find release in downloaders
|
|
nzbname = self.createNzbName(rel['info'], movie_dict)
|
|
|
|
found_release = False
|
|
for release_download in release_downloads:
|
|
found_release = False
|
|
if download_info.get('id'):
|
|
if release_download['id'] == download_info['id'] and release_download['downloader'] == download_info['downloader']:
|
|
log.debug('Found release by id: %s', release_download['id'])
|
|
found_release = True
|
|
break
|
|
else:
|
|
if release_download['name'] == nzbname or rel['info']['name'] in release_download['name'] or getImdb(release_download['name']) == getIdentifier(movie_dict):
|
|
log.debug('Found release by release name or imdb ID: %s', release_download['name'])
|
|
found_release = True
|
|
break
|
|
|
|
if not found_release:
|
|
log.info('%s not found in downloaders', nzbname)
|
|
|
|
#Check status if already missing and for how long, if > 1 week, set to ignored else to missing
|
|
if rel.get('status') == 'missing':
|
|
if rel.get('last_edit') < int(time.time()) - 7 * 24 * 60 * 60:
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True)
|
|
else:
|
|
# Set the release to missing
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'missing', single = True)
|
|
|
|
# Continue with next release
|
|
continue
|
|
|
|
# Log that we found the release
|
|
timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
|
|
log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
|
|
|
|
# Check status of release
|
|
if release_download['status'] == 'busy':
|
|
# Set the release to snatched if it was missing before
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'snatched', single = True)
|
|
|
|
# Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
|
|
if self.movieInFromFolder(release_download['folder']):
|
|
self.tagRelease(release_download = release_download, tag = 'downloading')
|
|
|
|
elif release_download['status'] == 'seeding':
|
|
#If linking setting is enabled, process release
|
|
if self.conf('file_action') != 'move' and not rel.get('status') == 'seeding' and self.statusInfoComplete(release_download):
|
|
log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio']))
|
|
|
|
# Remove the downloading tag
|
|
self.untagRelease(release_download = release_download, tag = 'downloading')
|
|
|
|
# Scan and set the torrent to paused if required
|
|
release_download.update({'pause': True, 'scan': True, 'process_complete': False})
|
|
scan_releases.append(release_download)
|
|
else:
|
|
#let it seed
|
|
log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio']))
|
|
|
|
# Set the release to seeding
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'seeding', single = True)
|
|
|
|
elif release_download['status'] == 'failed':
|
|
# Set the release to failed
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'failed', single = True)
|
|
|
|
fireEvent('download.remove_failed', release_download, single = True)
|
|
|
|
if self.conf('next_on_failed'):
|
|
fireEvent('movie.searcher.try_next_release', media_id = rel.get('media_id'))
|
|
|
|
elif release_download['status'] == 'completed':
|
|
log.info('Download of %s completed!', release_download['name'])
|
|
|
|
#Make sure the downloader sent over a path to look in
|
|
if self.statusInfoComplete(release_download):
|
|
|
|
# If the release has been seeding, process now the seeding is done
|
|
if rel.get('status') == 'seeding':
|
|
if self.conf('file_action') != 'move':
|
|
# Set the release to done as the movie has already been renamed
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'downloaded', single = True)
|
|
|
|
# Allow the downloader to clean-up
|
|
release_download.update({'pause': False, 'scan': False, 'process_complete': True})
|
|
scan_releases.append(release_download)
|
|
else:
|
|
# Scan and Allow the downloader to clean-up
|
|
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
|
scan_releases.append(release_download)
|
|
|
|
else:
|
|
# Set the release to snatched if it was missing before
|
|
fireEvent('release.update_status', rel.get('_id'), status = 'snatched', single = True)
|
|
|
|
# Remove the downloading tag
|
|
self.untagRelease(release_download = release_download, tag = 'downloading')
|
|
|
|
# Scan and Allow the downloader to clean-up
|
|
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
|
scan_releases.append(release_download)
|
|
else:
|
|
scan_required = True
|
|
|
|
except:
|
|
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
|
|
|
|
# The following can either be done here, or inside the scanner if we pass it scan_items in one go
|
|
for release_download in scan_releases:
|
|
# Ask the renamer to scan the item
|
|
if release_download['scan']:
|
|
if release_download['pause'] and self.conf('file_action') == 'link':
|
|
fireEvent('download.pause', release_download = release_download, pause = True, single = True)
|
|
self.scan(release_download = release_download)
|
|
if release_download['pause'] and self.conf('file_action') == 'link':
|
|
fireEvent('download.pause', release_download = release_download, pause = False, single = True)
|
|
if release_download['process_complete']:
|
|
# First make sure the files were successfully processed
|
|
if not self.hastagRelease(release_download = release_download, tag = 'failed_rename'):
|
|
# Remove the seeding tag if it exists
|
|
self.untagRelease(release_download = release_download, tag = 'renamed_already')
|
|
# Ask the downloader to process the item
|
|
fireEvent('download.process_complete', release_download = release_download, single = True)
|
|
|
|
if fire_scan and (scan_required or len(no_status_support) > 0):
|
|
self.scan()
|
|
|
|
self.checking_snatched = False
|
|
return True
|
|
except:
|
|
log.error('Failed checking snatched: %s', traceback.format_exc())
|
|
|
|
self.checking_snatched = False
|
|
return False
|
|
|
|
def extendReleaseDownload(self, release_download):
|
|
|
|
rls = None
|
|
db = get_db()
|
|
|
|
if release_download and release_download.get('id'):
|
|
try:
|
|
rls = db.get('release_download', '%s-%s' % (release_download.get('downloader'), release_download.get('id')), with_doc = True)['doc']
|
|
except:
|
|
log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader')))
|
|
|
|
if rls:
|
|
media = db.get('id', rls['media_id'])
|
|
release_download.update({
|
|
'imdb_id': getIdentifier(media),
|
|
'quality': rls['quality'],
|
|
'is_3d': rls['is_3d'],
|
|
'protocol': rls.get('info', {}).get('protocol') or rls.get('info', {}).get('type'),
|
|
'release_id': rls['_id'],
|
|
})
|
|
|
|
return release_download
|
|
|
|
def downloadIsTorrent(self, release_download):
|
|
return release_download and release_download.get('protocol') in ['torrent', 'torrent_magnet']
|
|
|
|
def fileIsAdded(self, src, group):
|
|
if not group or not group.get('before_rename'):
|
|
return False
|
|
return src in group['before_rename']
|
|
|
|
def moveTypeIsLinked(self):
|
|
return self.conf('default_file_action') in ['copy', 'link']
|
|
|
|
def statusInfoComplete(self, release_download):
|
|
return release_download.get('id') and release_download.get('downloader') and release_download.get('folder')
|
|
|
|
def movieInFromFolder(self, media_folder):
|
|
return media_folder and isSubFolder(media_folder, sp(self.conf('from'))) or not media_folder
|
|
|
|
def extractFiles(self, folder = None, media_folder = None, files = None, cleanup = False):
|
|
if not files: files = []
|
|
|
|
# RegEx for finding rar files
|
|
archive_regex = '(?P<file>^(?P<base>(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)'
|
|
restfile_regex = '(^%s\.(?:part(?!0*1\.rar$)\d+\.rar$|[rstuvw]\d+$))'
|
|
extr_files = []
|
|
|
|
from_folder = sp(self.conf('from'))
|
|
|
|
# Check input variables
|
|
if not folder:
|
|
folder = from_folder
|
|
|
|
check_file_date = True
|
|
if media_folder:
|
|
check_file_date = False
|
|
|
|
if not files:
|
|
for root, folders, names in os.walk(folder):
|
|
files.extend([sp(os.path.join(root, name)) for name in names])
|
|
|
|
# Find all archive files
|
|
archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)]
|
|
|
|
#Extract all found archives
|
|
for archive in archives:
|
|
# Check if it has already been processed by CPS
|
|
if self.hastagRelease(release_download = {'folder': os.path.dirname(archive['file']), 'files': archive['file']}):
|
|
continue
|
|
|
|
# Find all related archive files
|
|
archive['files'] = [name for name in files if re.search(restfile_regex % re.escape(archive['base']), name)]
|
|
archive['files'].append(archive['file'])
|
|
|
|
# Check if archive is fresh and maybe still copying/moving/downloading, ignore files newer than 1 minute
|
|
if check_file_date:
|
|
files_too_new, time_string = self.checkFilesChanged(archive['files'])
|
|
|
|
if files_too_new:
|
|
log.info('Archive seems to be still copying/moving/downloading or just copied/moved/downloaded (created on %s), ignoring for now: %s', (time_string, os.path.basename(archive['file'])))
|
|
continue
|
|
|
|
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
|
|
try:
|
|
rar_handle = RarFile(archive['file'], custom_path = self.conf('unrar_path'))
|
|
extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder))
|
|
self.makeDir(extr_path)
|
|
for packedinfo in rar_handle.infolist():
|
|
extr_file_path = sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
|
|
if not packedinfo.isdir and not os.path.isfile(extr_file_path):
|
|
log.debug('Extracting %s...', packedinfo.filename)
|
|
rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
|
|
if self.conf('unrar_modify_date'):
|
|
try:
|
|
os.utime(extr_file_path, (os.path.getatime(archive['file']), os.path.getmtime(archive['file'])))
|
|
except:
|
|
log.error('Rar modify date enabled, but failed: %s', traceback.format_exc())
|
|
extr_files.append(extr_file_path)
|
|
del rar_handle
|
|
except Exception as e:
|
|
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
|
|
continue
|
|
|
|
# Delete the archive files
|
|
for filename in archive['files']:
|
|
if cleanup:
|
|
try:
|
|
os.remove(filename)
|
|
except Exception as e:
|
|
log.error('Failed to remove %s: %s %s', (filename, e, traceback.format_exc()))
|
|
continue
|
|
files.remove(filename)
|
|
|
|
# Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
|
|
if extr_files and folder != from_folder:
|
|
for leftoverfile in list(files):
|
|
move_to = os.path.join(from_folder, os.path.relpath(leftoverfile, folder))
|
|
|
|
try:
|
|
self.makeDir(os.path.dirname(move_to))
|
|
self.moveFile(leftoverfile, move_to, cleanup)
|
|
except Exception as e:
|
|
log.error('Failed moving left over file %s to %s: %s %s', (leftoverfile, move_to, e, traceback.format_exc()))
|
|
# As we probably tried to overwrite the nfo file, check if it exists and then remove the original
|
|
if os.path.isfile(move_to) and os.path.getsize(leftoverfile) == os.path.getsize(move_to):
|
|
if cleanup:
|
|
log.info('Deleting left over file %s instead...', leftoverfile)
|
|
os.unlink(leftoverfile)
|
|
else:
|
|
continue
|
|
|
|
files.remove(leftoverfile)
|
|
extr_files.append(move_to)
|
|
|
|
if cleanup:
|
|
# Remove all left over folders
|
|
log.debug('Removing old movie folder %s...', media_folder)
|
|
self.deleteEmptyFolder(media_folder)
|
|
|
|
media_folder = os.path.join(from_folder, os.path.relpath(media_folder, folder))
|
|
folder = from_folder
|
|
|
|
if extr_files:
|
|
files.extend(extr_files)
|
|
|
|
# Cleanup files and folder if media_folder was not provided
|
|
if not media_folder:
|
|
files = []
|
|
folder = None
|
|
|
|
return folder, media_folder, files, extr_files
|
|
|
|
|
|
rename_options = {
|
|
'pre': '<',
|
|
'post': '>',
|
|
'choices': {
|
|
'ext': 'Extention (mkv)',
|
|
'namethe': 'Moviename, The',
|
|
'thename': 'The Moviename',
|
|
'year': 'Year (2011)',
|
|
'first': 'First letter (M)',
|
|
'quality': 'Quality (720p)',
|
|
'quality_type': '(HD) or (SD)',
|
|
'3d': '3D',
|
|
'3d_type': '3D Type (Full SBS)',
|
|
'video': 'Video (x264)',
|
|
'audio': 'Audio (DTS)',
|
|
'group': 'Releasegroup name',
|
|
'source': 'Source media (Bluray)',
|
|
'resolution_width': 'resolution width (1280)',
|
|
'resolution_height': 'resolution height (720)',
|
|
'audio_channels': 'audio channels (7.1)',
|
|
'original': 'Original filename',
|
|
'original_folder': 'Original foldername',
|
|
'imdb_id': 'IMDB id (tt0123456)',
|
|
'cd': 'CD number (cd1)',
|
|
'cd_nr': 'Just the cd nr. (1)',
|
|
'mpaa': 'MPAA or other certification',
|
|
'mpaa_only': 'MPAA only certification (G|PG|PG-13|R|NC-17|Not Rated)',
|
|
'category': 'Category label',
|
|
},
|
|
}
|
|
|
|
config = [{
|
|
'name': 'renamer',
|
|
'order': 40,
|
|
'description': 'Move and rename your downloaded movies to your movie directory.',
|
|
'groups': [
|
|
{
|
|
'tab': 'renamer',
|
|
'name': 'renamer',
|
|
'label': 'Rename downloaded movies',
|
|
'wizard': True,
|
|
'options': [
|
|
{
|
|
'name': 'enabled',
|
|
'default': False,
|
|
'type': 'enabler',
|
|
},
|
|
{
|
|
'name': 'from',
|
|
'type': 'directory',
|
|
'description': 'Folder where CP searches for movies.',
|
|
},
|
|
{
|
|
'name': 'to',
|
|
'type': 'directory',
|
|
'description': 'Default folder where the movies are moved to.',
|
|
},
|
|
{
|
|
'name': 'folder_name',
|
|
'label': 'Folder naming',
|
|
'description': 'Name of the folder. Keep empty for no folder.',
|
|
'default': '<namethe> (<year>)',
|
|
'type': 'choice',
|
|
'options': rename_options
|
|
},
|
|
{
|
|
'name': 'file_name',
|
|
'label': 'File naming',
|
|
'description': 'Name of the file',
|
|
'default': '<thename><cd>.<ext>',
|
|
'type': 'choice',
|
|
'options': rename_options
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'replace_doubles',
|
|
'type': 'bool',
|
|
'label': 'Consider Missing Data',
|
|
'description': 'Attempt to clean up double separaters due to missing data for fields',
|
|
'default': True
|
|
},
|
|
{
|
|
'name': 'unrar',
|
|
'type': 'bool',
|
|
'description': 'Extract rar files if found.',
|
|
'default': False,
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'unrar_path',
|
|
'description': 'Custom path to unrar bin',
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'unrar_modify_date',
|
|
'type': 'bool',
|
|
'description': ('Set modify date of unrar-ed files to the rar-file\'s date.', 'This will allow XBMC to recognize extracted files as recently added even if the movie was released some time ago.'),
|
|
'default': False,
|
|
},
|
|
{
|
|
'name': 'cleanup',
|
|
'type': 'bool',
|
|
'description': 'Cleanup leftover files after successful rename.',
|
|
'default': False,
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'run_every',
|
|
'label': 'Run every',
|
|
'default': 1,
|
|
'type': 'int',
|
|
'unit': 'min(s)',
|
|
'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled'),
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'force_every',
|
|
'label': 'Force every',
|
|
'default': 2,
|
|
'type': 'int',
|
|
'unit': 'hour(s)',
|
|
'description': 'Forces the renamer to scan every X hours',
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'next_on_failed',
|
|
'default': True,
|
|
'type': 'bool',
|
|
'description': 'Try the next best release for a movie after a download failed.',
|
|
},
|
|
{
|
|
'name': 'move_leftover',
|
|
'type': 'bool',
|
|
'description': 'Move all leftover file after renaming, to the movie folder.',
|
|
'default': False,
|
|
'advanced': True,
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'separator',
|
|
'label': 'File-Separator',
|
|
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'foldersep',
|
|
'label': 'Folder-Separator',
|
|
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
|
},
|
|
{
|
|
'name': 'check_space',
|
|
'label': 'Check space',
|
|
'default': True,
|
|
'type': 'bool',
|
|
'description': ('Check if there\'s enough available space to rename the files', 'Disable when the filesystem doesn\'t return the proper value'),
|
|
'advanced': True,
|
|
},
|
|
{
|
|
'name': 'default_file_action',
|
|
'label': 'Default File Action',
|
|
'default': 'move',
|
|
'type': 'dropdown',
|
|
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
|
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.',
|
|
'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.'),
|
|
'advanced': True,
|
|
},
|
|
{
|
|
'name': 'file_action',
|
|
'label': 'Torrent File Action',
|
|
'default': 'link',
|
|
'type': 'dropdown',
|
|
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
|
'description': 'See above. It is prefered to use link when downloading torrents as it will save you space, while still beeing able to seed.',
|
|
'advanced': True,
|
|
},
|
|
{
|
|
'advanced': True,
|
|
'name': 'ntfs_permission',
|
|
'label': 'NTFS Permission',
|
|
'type': 'bool',
|
|
'hidden': os.name != 'nt',
|
|
'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).',
|
|
'default': False,
|
|
},
|
|
],
|
|
}, {
|
|
'tab': 'renamer',
|
|
'name': 'meta_renamer',
|
|
'label': 'Advanced renaming',
|
|
'description': 'Meta data file renaming. Use <filename> to use the above "File naming" settings, without the file extention.',
|
|
'advanced': True,
|
|
'options': [
|
|
{
|
|
'name': 'rename_nfo',
|
|
'label': 'Rename .NFO',
|
|
'description': 'Rename original .nfo file',
|
|
'type': 'bool',
|
|
'default': True,
|
|
},
|
|
{
|
|
'name': 'nfo_name',
|
|
'label': 'NFO naming',
|
|
'default': '<filename>.orig.<ext>',
|
|
'type': 'choice',
|
|
'options': rename_options
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}]
|