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:
                unrar_path = self.conf('unrar_path')
                unrar_path = unrar_path if unrar_path and (os.path.isfile(unrar_path) or re.match('^[a-zA-Z0-9_/\.\-]+$', unrar_path)) else None

                rar_handle = RarFile(archive['file'], custom_path = 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/copied/linked 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': 'Clean Name',
                    'description': ('Attempt to clean up double separaters due to missing data for fields.','Sometimes this eliminates wanted white space (see <a href="https://github.com/RuudBurger/CouchPotatoServer/issues/2782">#2782</a>).'),
                    '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 &lt;filename&gt; 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
                },
            ],
        },
    ],
}]
