Esempio n. 1
0
def delete(location):
    """
    Delete a file from the file system. Also supports stacked movie files.

    Example:
        success = delete(location)

    :type location: unicode
    :param location: the path to the file you wish to delete.
    :rtype: bool
    :return: True if (at least one) file was deleted successfully, False otherwise.
    """
    debug("Attempting to delete {0}".format(location))

    paths = split_stack(location)
    success = []

    for p in paths:
        if xbmcvfs.exists(p):
            success.append(bool(xbmcvfs.delete(p)))
        else:
            debug(f"File {p} no longer exists.", xbmc.LOGERROR)
            success.append(False)

    return any(success)
Esempio n. 2
0
    def execute_query(self, query):
        """TODO: Docstring
        """
        response = xbmc.executeJSONRPC(json.dumps(query))
        debug(f"[{query['method']}] Response: {response}")

        return self.check_errors(response)
Esempio n. 3
0
    def prepare_query(self, video_type):
        """TODO: Docstring
        :rtype dict:
        :return the complete JSON-RPC request to be sent
        """
        # Always refresh the user's settings before preparing a JSON-RPC query
        self.settings = reload_preferences()

        # A non-exhaustive list of pre-defined filters to use during JSON-RPC requests
        # These are possible conditions that must be met before a video can be deleted
        by_playcount = {"field": "playcount", "operator": "greaterthan", "value": "0"}
        by_date_played = {"field": "lastplayed", "operator": "notinthelast", "value": f"{self.settings[expire_after]:f}"}
        by_minimum_rating = {"field": "rating", "operator": "lessthan", "value": f"{self.settings[minimum_rating]:f}"}
        by_no_rating = {"field": "rating", "operator": "isnot", "value": "0"}
        by_progress = {"field": "inprogress", "operator": "false", "value": ""}
        by_exclusion1 = {"field": "path", "operator": "doesnotcontain", "value": self.settings[exclusion1]}
        by_exclusion2 = {"field": "path", "operator": "doesnotcontain", "value": self.settings[exclusion2]}
        by_exclusion3 = {"field": "path", "operator": "doesnotcontain", "value": self.settings[exclusion3]}
        by_exclusion4 = {"field": "path", "operator": "doesnotcontain", "value": self.settings[exclusion4]}
        by_exclusion5 = {"field": "path", "operator": "doesnotcontain", "value": self.settings[exclusion5]}

        # link settings and filters together
        settings_and_filters = [
            (self.settings[enable_expiration], by_date_played),
            (self.settings[clean_when_low_rated], by_minimum_rating),
            (self.settings[not_in_progress], by_progress),
            (self.settings[exclusion_enabled] and self.settings[exclusion1] != "", by_exclusion1),
            (self.settings[exclusion_enabled] and self.settings[exclusion2] != "", by_exclusion2),
            (self.settings[exclusion_enabled] and self.settings[exclusion3] != "", by_exclusion3),
            (self.settings[exclusion_enabled] and self.settings[exclusion4] != "", by_exclusion4),
            (self.settings[exclusion_enabled] and self.settings[exclusion5] != "", by_exclusion5)
        ]

        # Only check not rated videos if checking for video ratings at all
        if self.settings[clean_when_low_rated]:
            settings_and_filters.append((self.settings[ignore_no_rating], by_no_rating))

        enabled_filters = [by_playcount]
        for setting, filter in settings_and_filters:
            if setting and filter["field"] in self.supported_filter_fields[video_type]:
                enabled_filters.append(filter)

        debug(f"[{self.methods[video_type]}] Filters enabled: {enabled_filters}")

        filters = {"and": enabled_filters}

        request = {
            "jsonrpc": "2.0",
            "method": self.methods[video_type],
            "params": {
                "properties": self.properties[video_type],
                "filter": filters
            },
            "id": 1
        }

        return request
Esempio n. 4
0
    def get(self):
        """
        Retrieve the contents of the log file. Creates a new log if none is found.

        :rtype: unicode
        :return: The contents of the log file.
        """
        try:
            debug("Retrieving log file contents.")
            with open(self.logpath, "r", encoding="utf-8") as f:
                contents = f.read()
        except (IOError, OSError) as err:
            debug(f"{err}", xbmc.LOGERROR)
        else:
            return contents
Esempio n. 5
0
    def clear(self):
        """
        Erase the contents of the log file.

        :rtype: unicode
        :return: An empty string if clearing succeeded.
        """
        try:
            debug("Clearing log file contents.")
            with open(self.logpath, "r+", encoding="utf-8") as f:
                f.truncate()
        except (IOError, OSError) as err:
            debug(f"{err}", xbmc.LOGERROR)
        else:
            return self.get()
Esempio n. 6
0
def is_hardlinked(filename):
    """
    Tests the provided filename for hard links and only returns True if the number of hard links is exactly 1.

    :param filename: The filename to check for hard links
    :type filename: str
    :return: True if the number of hard links equals 1, False otherwise.
    :rtype: bool
    """
    if get_value(keep_hard_linked):
        debug("Hard link checks are enabled.")
        return not all(i == 1 for i in map(xbmcvfs.Stat.st_nlink, map(xbmcvfs.Stat, split_stack(filename))))
    else:
        debug("Hard link checks are disabled.")
        return False
Esempio n. 7
0
    def check_errors(response):
        """TODO: Docstring
        """
        result = json.loads(response)

        try:
            error = result["error"]
            debug(f"An error occurred. {error}", xbmc.LOGERROR)
            raise ValueError(f"[{error['data']['method']}]: {error['data']['stack']['message']}")
        except KeyError as ke:
            if "error" in str(ke):
                pass  # no error
            else:
                raise KeyError(f"Something went wrong while parsing errors from JSON-RPC. I couldn't find {ke}") from ke

        # No errors, so return actual response
        return result["result"]
Esempio n. 8
0
    def clean_category(self, video_type, progress_dialog):
        """
        Clean all watched videos of the provided type.

        :type video_type: unicode
        :param video_type: The type of videos to clean (one of TVSHOWS, MOVIES, MUSIC_VIDEOS).
        :param progress_dialog: The dialog that is used to display the progress in
        :type progress_dialog: DialogProgress
        :rtype: (list, int)
        :return: A list of the filenames that were cleaned and the return status.
        """

        # Reset counters
        cleaned_files = []

        for filename, title in self.db.get_expired_videos(video_type):
            # Check at the beginning of each loop if the user pressed cancel
            # We do not want to cancel cleaning in the middle of a cycle to prevent issues with leftovers
            if self.user_aborted(progress_dialog):
                self.exit_status = self.STATUS_ABORTED
                progress_dialog.close()
                break
            else:
                if file_exists(filename) and not is_hardlinked(filename):
                    if not self.silent:
                        file_names = "\n".join(map(os.path.basename, split_stack(filename)))
                        heading = translate(32618).format(type=LOCALIZED_VIDEO_TYPES[video_type])
                        progress_dialog.update(0, f"{heading}\n{file_names}")
                        self.monitor.waitForAbort(2)

                    cleaned_files.extend(self.process_file(filename, title))
                else:
                    debug(f"Not cleaning {filename}. It may have already been removed.", xbmc.LOGWARNING)

        else:
            if not self.silent:
                progress_dialog.update(100, translate(32616).format(type=LOCALIZED_VIDEO_TYPES[video_type]))
                self.monitor.waitForAbort(2)

            if self.user_aborted(progress_dialog):
                # Prevent another dialog from appearing if the user aborts
                # after all of this video_type were already cleaned
                self.exit_status = self.STATUS_ABORTED

        return cleaned_files, self.exit_status
Esempio n. 9
0
def anonymize_path(path):
    """
    :type path: unicode
    :param path: The network path containing credentials that need to be stripped.
    :rtype: unicode
    :return: The network path without the credentials.
    """
    if "://" in path:
        kodi.debug(f"Anonymizing {path}")
        # Look for anything matching a protocol followed by credentials
        # This regex assumes there is no @ in the remainder of the path
        regex = "^(?P<protocol>smb|nfs|afp|upnp|http|https):\/\/(.+:.+@)?(?P<path>[^@]+?)$"
        results = re.match(regex, path, flags=re.I | re.U).groupdict()

        # Keep only the protocol and the actual path
        path = f"{results['protocol']}://{results['path']}"
        kodi.debug(f"Result: {path}")

    return path
Esempio n. 10
0
def autostart():
    """
    Starts the cleaning service.
    """
    janitor = Janitor()

    service_sleep = 4  # Lower than 4 causes too much stress on resource limited systems such as RPi
    ticker = 0
    delayed_completed = False

    while not janitor.monitor.abortRequested():
        if get_value(service_enabled):
            scan_interval_ticker = get_value(
                scan_interval) * 60 / service_sleep
            delayed_start_ticker = get_value(
                delayed_start) * 60 / service_sleep

            if delayed_completed and ticker >= scan_interval_ticker:
                results, _ = janitor.clean()
                if janitor.exit_status == janitor.STATUS_SUCCESS and len(
                        results) > 0:
                    notify(translate(32518).format(amount=len(results)))
                ticker = 0
            elif not delayed_completed and ticker >= delayed_start_ticker:
                delayed_completed = True
                results, _ = janitor.clean()
                if janitor.exit_status == janitor.STATUS_SUCCESS and len(
                        results) > 0:
                    notify(translate(32518).format(amount=len(results)))
                ticker = 0

            janitor.monitor.waitForAbort(service_sleep)
            ticker += 1
        else:
            janitor.monitor.waitForAbort(service_sleep)

    debug(u"Abort requested. Terminating.")
    return
Esempio n. 11
0
    def process_file(self, file_name, title):
        """Handle the cleaning of a video file, either via deletion or moving to another location

        :param file_name:
        :type file_name:
        :param title:
        :type title:
        :return:
        :rtype:
        """
        cleaned_files = []
        if get_value(cleaning_type) == self.CLEANING_TYPE_RECYCLE:
            # Recycle bin not set up, prompt user to set up now
            if get_value(recycle_bin) == "":
                self.exit_status = self.STATUS_ABORTED
                if Dialog().yesno(ADDON_NAME, translate(32521)):
                    xbmc.executebuiltin(f"Addon.OpenSettings({ADDON_ID})")
                return []
            if get_value(create_subdirs):
                title = re.sub(r"[\\/:*?\"<>|]+", "_", title)
                new_path = os.path.join(get_value(recycle_bin), title)
            else:
                new_path = get_value(recycle_bin)
            if recycle(file_name, new_path):
                debug("File(s) recycled successfully")
                cleaned_files.extend(split_stack(file_name))
                self.clean_extras(file_name, new_path)
                delete_empty_folders(os.path.dirname(file_name))
                self.exit_status = self.STATUS_SUCCESS
                return cleaned_files
            else:
                debug("Errors occurred while recycling. Skipping related files and directories.", xbmc.LOGWARNING)
                Dialog().ok(translate(32611), translate(32612))
                self.exit_status = self.STATUS_FAILURE
                return cleaned_files
        elif get_value(cleaning_type) == self.CLEANING_TYPE_DELETE:
            if delete(file_name):
                debug("File(s) deleted successfully")
                cleaned_files.extend(split_stack(file_name))
                self.clean_extras(file_name)
                delete_empty_folders(os.path.dirname(file_name))
                self.exit_status = self.STATUS_SUCCESS
            else:
                debug("Errors occurred during file deletion", xbmc.LOGWARNING)
                self.exit_status = self.STATUS_FAILURE

            return cleaned_files
Esempio n. 12
0
    def clean_library(self, purged_files):
        # Check if we need to perform any post-cleaning operations
        if purged_files and get_value(clean_library):
            self.monitor.waitForAbort(2)  # Sleep 2 seconds to make sure file I/O is done.

            if xbmc.getCondVisibility("Library.IsScanningVideo"):
                debug("The video library is being updated. Skipping library cleanup.", xbmc.LOGWARNING)
            else:
                debug("Starting Kodi library cleaning")
                xbmc.executebuiltin("CleanLibrary(video)")
        else:
            debug("Cleaning Kodi library not required and/or not enabled.")
Esempio n. 13
0
    def get_expired_videos(self, video_type):
        """
        Find videos in the Kodi library that have been watched.

        Respects any other conditions user enables in the addon's settings.

        :type video_type: unicode
        :param video_type: The type of videos to find (one of the globals MOVIES, MUSIC_VIDEOS or TVSHOWS).
        :rtype: list
        :return: A list of expired videos, along with a number of extra attributes specific to the video type.
        """

        # TODO: split up this method into a pure database query and let the Janitor class handle the rest

        video_types = (TVSHOWS, MOVIES, MUSIC_VIDEOS)
        setting_types = (clean_tv_shows, clean_movies, clean_music_videos)

        # TODO: Is this loop still required? Maybe settings_types[video_type] is sufficient?
        for type, setting in zip(video_types, setting_types):
            if type == video_type and get_value(setting):
                # Do the actual work here
                query = self.prepare_query(video_type)
                result = self.execute_query(query)

                try:
                    debug(f"Found {result['limits']['total']} watched {video_type} matching your conditions")
                    debug(f"JSON Response: {result}")
                    for video in result[video_type]:
                        # Gather all properties and add it to this video's information
                        temp = []
                        for p in self.properties[video_type]:
                            temp.append(video[p])
                        yield temp
                except KeyError as ke:
                    if video_type in str(ke):
                        pass  # no expired videos found
                    else:
                        raise KeyError(f"Could not find key {ke} in response.") from ke
                finally:
                    debug("Breaking the loop")
                    break  # Stop looping after the first match for video_type
Esempio n. 14
0
    def clean(self):
        """
        Clean up any watched videos in the Kodi library, satisfying any conditions set via the addon settings.

        :rtype: (dict, int)
        :return: A single-line (localized) summary of the cleaning results to be used for a notification, plus a status.
        """
        debug("Starting cleaning routine.")

        if get_value(clean_when_idle) and xbmc.Player().isPlaying():
            debug("Kodi is currently playing a file. Skipping cleaning.", xbmc.LOGWARNING)
            return None, self.exit_status

        cleaning_results = []

        if not get_value(clean_when_low_disk_space) or (get_value(clean_when_low_disk_space) and disk_space_low()):
            for video_type in KNOWN_VIDEO_TYPES:
                if self.exit_status != self.STATUS_ABORTED:
                    progress = DialogProgress()
                    if not self.silent:
                        progress.create(f"{ADDON_NAME} - {LOCALIZED_VIDEO_TYPES[video_type].capitalize()}")
                        progress_text = f"{translate(32618).format(type=LOCALIZED_VIDEO_TYPES[video_type])}"
                        progress.update(0, progress_text)
                        self.monitor.waitForAbort(2)
                    if self.user_aborted(progress):
                        progress.close()
                        break
                    else:
                        cleaned_files, status = self.clean_category(video_type, progress)
                        cleaning_results.extend(cleaned_files)
                        if not self.silent:
                            progress.close()
                else:
                    debug("User aborted")
                    break

        self.clean_library(cleaning_results)

        Log().prepend(cleaning_results)

        return cleaning_results, self.exit_status
Esempio n. 15
0
def delete_empty_folders(location):
    """
    Delete the folder if it is empty. Presence of custom file extensions can be ignored while scanning.

    To achieve this, edit the ignored file types setting in the addon settings.

    Example:
        success = delete_empty_folders(path)

    :type location: unicode
    :param location: The path to the folder to be deleted.
    :rtype: bool
    :return: True if the folder was deleted successfully, False otherwise.
    """
    if not get_value(delete_folders):
        debug("Deleting of empty folders is disabled.")
        return False

    folder = split_stack(location)[0]  # Stacked paths should have the same parent, use any
    debug(f"Checking if {folder} is empty")
    ignored_file_types = [file_ext.strip() for file_ext in get_value(ignore_extensions).split(",")]
    debug(f"Ignoring file types {ignored_file_types}")

    subfolders, files = xbmcvfs.listdir(folder)

    empty = True
    try:
        for f in files:
            _, ext = os.path.splitext(f)
            if ext and ext not in ignored_file_types:  # ensure f is not a folder and its extension is not ignored
                debug(f"Found non-ignored file type {ext}")
                empty = False
                break
    except OSError as oe:
        debug(f"Error deriving file extension. Errno {oe.errno}", xbmc.LOGERROR)
        empty = False

    # Only delete directories if we found them to be empty (containing no files or filetypes we ignored)
    if empty:
        debug("Directory is empty and will be removed")
        try:
            # Recursively delete any subfolders
            for f in subfolders:
                debug(f"Deleting file at {os.path.join(folder, f)}")
                delete_empty_folders(os.path.join(folder, f))

            # Delete any files in the current folder
            for f in files:
                debug(f"Deleting file at {os.path.join(folder, f)}")
                xbmcvfs.delete(os.path.join(folder, f))

            # Finally delete the current folder
            return xbmcvfs.rmdir(folder)
        except OSError as oe:
            debug(f"An exception occurred while deleting folders. Errno {oe.errno}", xbmc.LOGERROR)
            return False
    else:
        debug("Directory is not empty and will not be removed")
        return False
Esempio n. 16
0
def recycle(source, dest_folder):
    """Move a file to a new destination. Will create destination if it does not exist.

    Example:
        result = recycle(a, b)

    :type source: unicode
    :param source: the source path (absolute)
    :type dest_folder: unicode
    :param dest_folder: the destination path (absolute)
    :rtype: bool
    :return: True if (all stacked) files were moved, False otherwise
    """
    paths = split_stack(source)
    files_moved_successfully = 0
    dest_folder = xbmcvfs.makeLegalFilename(dest_folder)

    for p in paths:
        debug(f"Attempting to move {p} to {dest_folder}.")
        if xbmcvfs.exists(p):
            if not xbmcvfs.exists(dest_folder):
                if xbmcvfs.mkdirs(dest_folder):
                    debug(f"Created destination {dest_folder}.")
                else:
                    debug(f"Destination {dest_folder} could not be created.", xbmc.LOGERROR)
                    return False

            new_path = os.path.join(dest_folder, os.path.basename(p))

            if xbmcvfs.exists(new_path):
                debug("A file with the same name already exists in the holding folder. Checking file sizes.")
                existing_file = xbmcvfs.File(new_path)
                file_to_move = xbmcvfs.File(p)
                if file_to_move.size() > existing_file.size():
                    debug("This file is larger than the existing file. Replacing it with this one.")
                    existing_file.close()
                    file_to_move.close()
                    if bool(xbmcvfs.delete(new_path) and bool(xbmcvfs.rename(p, new_path))):
                        files_moved_successfully += 1
                    else:
                        return False
                else:
                    debug("This file isn't larger than the existing file. Deleting it instead of moving.")
                    existing_file.close()
                    file_to_move.close()
                    if bool(xbmcvfs.delete(p)):
                        files_moved_successfully += 1
                    else:
                        return False
            else:
                debug(f"Moving {p} to {new_path}.")
                move_success = bool(xbmcvfs.rename(p, new_path))
                copy_success, delete_success = False, False
                if not move_success:
                    debug("Move failed, falling back to copy and delete.", xbmc.LOGWARNING)
                    copy_success = bool(xbmcvfs.copy(p, new_path))
                    if copy_success:
                        debug("Copied successfully, attempting delete of source file.")
                        delete_success = bool(xbmcvfs.delete(p))
                        if not delete_success:
                            debug("Could not remove source file. Please remove the file manually.", xbmc.LOGWARNING)
                    else:
                        debug("Copying failed, please make sure you have appropriate permissions.", xbmc.LOGFATAL)
                        return False

                if move_success or (copy_success and delete_success):
                    files_moved_successfully += 1

        else:
            debug(f"File {p} is no longer available.", xbmc.LOGWARNING)

    return len(paths) == files_moved_successfully
Esempio n. 17
0
def get_free_disk_space(path):
    """Determine the percentage of free disk space.

    :type path: unicode
    :param path: The path to the drive to check. This can be any path of any depth on the desired drive.
    :rtype: float
    :return: The percentage of free space on the disk; 100% if errors occur.
    """
    percentage = float(100)
    debug(f"Checking for disk space on path: {path}")
    if xbmcvfs.exists(path.encode()):
        if xbmc.getCondVisibility("System.Platform.Windows"):
            debug("We are checking disk space from a Windows file system")
            debug(f"The path to check is {path}")

            if "://" in path:
                debug("We are dealing with network paths")
                debug(f"Extracting information from share {path}")

                regex = "(?P<type>smb|nfs|afp)://(?:(?P<user>.+):(?P<pass>.+)@)?(?P<host>.+?)/(?P<share>[^\/]+).*$"
                pattern = re.compile(regex, flags=re.I | re.U)
                match = pattern.match(path)
                try:
                    share = match.groupdict()
                    debug(f"Protocol: {share['type']}, User: {share['user']}, Password: {share['pass']}, Host: {share['host']}, Share: {share['share']}")
                except KeyError as ke:
                    debug(f"Could not parse {ke} from {path}.", xbmc.LOGERROR)
                    return percentage

                debug("Creating UNC paths so Windows understands the shares")
                path = os.path.normcase(os.sep + os.sep + share["host"] + os.sep + share["share"])
                debug(f"UNC path: {path}")
                debug("If checks fail because you need credentials, please mount the share first")
            else:
                debug("We are dealing with local paths")

            bytes_total = c_ulonglong(0)
            bytes_free = c_ulonglong(0)
            windll.kernel32.GetDiskFreeSpaceExW(c_wchar_p(path), byref(bytes_free), byref(bytes_total), None)

            try:
                percentage = float(bytes_free.value) / float(bytes_total.value) * 100
                debug("Hard disk check results:")
                debug(f"Bytes free: {bytes_free.value}")
                debug(f"Bytes total: {bytes_total.value}")
            except ZeroDivisionError:
                notify(translate(32511), 15000, level=xbmc.LOGERROR)
        else:
            debug("We are checking disk space from a non-Windows file system")
            debug(f"Stripping {path} of all redundant stuff.")
            path = os.path.normpath(path)
            debug(f"The path now is {path}")

            try:
                diskstats = os.statvfs(path)
                percentage = float(diskstats.f_bfree) / float(diskstats.f_blocks) * 100
                debug("Hard disk check results:")
                debug(f"Bytes free: {diskstats.f_bfree}")
                debug(f"Bytes total: {diskstats.f_blocks}")
            except OSError as ose:
                # TODO: Linux cannot check remote share disk space yet
                # notify(translate(32512), 15000, level=xbmc.LOGERROR)
                notify(translate(32524), 15000, level=xbmc.LOGERROR)
                debug(f"Error accessing {path}: {ose}")
            except ZeroDivisionError:
                notify(translate(32511), 15000, level=xbmc.LOGERROR)
    else:
        notify(translate(32513), 15000, level=xbmc.LOGERROR)

    debug(f"Free space: {percentage:.2f}%")
    return percentage
Esempio n. 18
0
 def __init__(self):
     debug(f"{ADDON.getAddonInfo('name')} version {ADDON.getAddonInfo('version')} loaded.")
     self.db = Database()
Esempio n. 19
0
    def trim(self, lines_to_keep=25):
        """
        Trim the log file to contain a maximum number of lines.

        :type lines_to_keep: int
        :param lines_to_keep: The number of lines to preserve. Any lines beyond this number get erased. Defaults to 25.
        :rtype: unicode
        :return: The contents of the log file after trimming.
        """
        try:
            debug("Trimming log file contents.")
            with open(self.logpath, encoding="utf-8") as f:
                debug(f"Saving the top {lines_to_keep} lines.")
                lines = []
                for i in range(lines_to_keep):
                    lines.append(f.readline())
        except (IOError, OSError) as err:
            debug(f"{err}", xbmc.LOGERROR)
        else:
            try:
                debug("Removing all log contents.")
                with open(self.logpath, "w", encoding="utf-8") as f:
                    debug("Restoring saved log contents.")
                    f.writelines(lines)
            except (IOError, OSError) as err:
                debug(f"{err}", xbmc.LOGERROR)
            else:
                return self.get()
Esempio n. 20
0
    def prepend(self, data):
        """
        Prepend the given data to the current log file. Will create a new log file if none exists.

        :type data: list
        :param data: A list of strings to prepend to the log file.
        """
        if data:
            previous_data = ""
            debug("Prepending the log file with new data")
            try:
                debug("Backing up current log")
                with open(self.logpath, "r", encoding="utf-8") as f:
                    previous_data = f.read()
            except (IOError, OSError, FileNotFoundError) as err:
                debug(f"{err}", xbmc.LOGERROR)
                debug("Assuming there is no previous log data")
                previous_data = ""
            finally:
                try:
                    with open(self.logpath, "w", encoding="utf-8") as f:
                        debug("Writing new log data")
                        f.write(f"[B][{time.strftime('%d/%m/%Y  -  %H:%M:%S')}][/B]\n")
                        for line in data:
                            f.write(f" • {line}\n")
                        f.write("\n")

                        debug("Appending previous log file contents")
                        f.writelines(previous_data)
                except (IOError, OSError) as err:
                    debug(f"{err}", xbmc.LOGERROR)
        else:
            debug("Nothing to log")
Esempio n. 21
0
    def clean_extras(self, source, dest_folder=None):
        """Clean files related to another file based on the user's preferences.

        Related files are files that only differ by extension, or that share a prefix in case of stacked movies.

        Examples of related files include NFO files, thumbnails, subtitles, fanart, etc.

        :type source: unicode
        :param source: Location of the file whose related files should be cleaned.
        :type dest_folder: unicode
        :param dest_folder: (Optional) The folder where related files should be moved to. Not needed when deleting.
        """
        if get_value(clean_related):
            debug("Cleaning related files.")

            path_list = split_stack(source)
            path, name = os.path.split(path_list[0])  # Because stacked movies are in the same folder, only check one
            if is_stacked_file(source):
                name = get_common_prefix(path_list)
            else:
                name, ext = os.path.splitext(name)

            debug(f"Attempting to match related files in {path} with prefix {name}")
            for extra_file in xbmcvfs.listdir(path)[1]:
                if extra_file.startswith(name):
                    debug(f"{extra_file} starts with {name}.")
                    extra_file_path = os.path.join(path, extra_file)
                    if get_value(cleaning_type) == self.CLEANING_TYPE_DELETE:
                        if extra_file_path not in path_list:
                            debug(f"Deleting {extra_file_path}.")
                            xbmcvfs.delete(extra_file_path)
                    elif get_value(cleaning_type) == self.CLEANING_TYPE_RECYCLE:
                        new_extra_path = os.path.join(dest_folder, os.path.basename(extra_file))
                        if new_extra_path not in path_list:
                            debug(f"Moving {extra_file_path} to {new_extra_path}.")
                            xbmcvfs.rename(extra_file_path, new_extra_path)
            debug("Finished searching for related files.")
        else:
            debug("Cleaning of related files is disabled.")