示例#1
0
class Recipe(object):
    plex = None
    trakt = None
    tmdb = None
    tvdb = None

    def __init__(self, recipe_name, sort_only=False, config_file=None):
        self.recipe_name = recipe_name

        self.config = ConfigParser(config_file)
        self.recipe = RecipeParser(recipe_name)

        if self.recipe['library_type'].lower().startswith('movie'):
            self.library_type = 'movie'
        elif self.recipe['library_type'].lower().startswith('tv'):
            self.library_type = 'tv'
        else:
            raise Exception("Library type should be 'movie' or 'tv'")

        self.source_library_config = self.recipe['source_libraries']

        self.plex = plexutils.Plex(self.config['plex']['baseurl'],
                                   self.config['plex']['token'])

        if self.config['trakt']['username']:
            self.trakt = traktutils.Trakt(
                self.config['trakt']['username'],
                client_id=self.config['trakt']['client_id'],
                client_secret=self.config['trakt']['client_secret'],
                oauth_token=self.config['trakt'].get('oauth_token', ''),
                oauth=self.recipe.get('trakt_oauth', False),
                config=self.config)
            if self.trakt.oauth_token:
                self.config['trakt']['oauth_token'] = self.trakt.oauth_token

        if self.config['tmdb']['api_key']:
            self.tmdb = tmdb.TMDb(self.config['tmdb']['api_key'],
                                  cache_file=self.config['tmdb']['cache_file'])

        if self.config['tvdb']['username']:
            self.tvdb = tvdb.TheTVDB(self.config['tvdb']['username'],
                                     self.config['tvdb']['api_key'],
                                     self.config['tvdb']['user_key'])

    def _run(self):
        item_list = []  # TODO Replace with dict, scrap item_ids?
        item_ids = []
        force_imdb_id_match = False
        max_count = self.recipe['new_library']['max_count']
        #remove symlinks
        ln_dir = self.recipe['new_library']['folder']
        if os.path.exists(ln_dir):
            print('Removing symlinks folder {}'.format(ln_dir))
            try:
                shutil.rmtree(ln_dir)
            except:
                print("Shit something went wrong.")
        # Get the trakt lists
        for url in self.recipe['source_list_urls']:
            netloc = urlparse(url).netloc
            if 'api.trakt.tv' in netloc:
                if max_count > 0:
                    if not (url.find('?limit=') != -1):
                        url = url + "?limit={}".format(max_count)
                (item_list, item_ids) = self.trakt.add_items(
                    self.library_type, url, item_list, item_ids,
                    self.recipe['new_library']['max_age'] or 0)
            elif not 'api.trakt.tv' in netloc:
                data = urlparse(url).path.split("/")
                if max_count > 0:
                    if self.library_type == "tv":
                        url = "https://api.trakt.tv/users/{}/lists/{}/items/{}?limit={}".format(
                            data[2], data[4],
                            self.library_type.replace('tv', 'shows'),
                            max_count)
                    else:
                        url = "https://api.trakt.tv/users/{}/lists/{}/items/{}?limit={}".format(
                            data[2], data[4], self.library_type, max_count)
                    (item_list, item_ids) = self.trakt.add_items(
                        self.library_type, url, item_list, item_ids,
                        self.recipe['new_library']['max_age'] or 0)
                else:
                    if self.library_type == "tv":
                        url = "https://api.trakt.tv/users/{}/lists/{}/items/{}".format(
                            data[2], data[4],
                            self.library_type.replace('tv', 'shows'))
                    else:
                        url = "https://api.trakt.tv/users/{}/lists/{}/items/{}".format(
                            data[2], data[4], self.library_type)
                    (item_list, item_ids) = self.trakt.add_items(
                        self.library_type, url, item_list, item_ids,
                        self.recipe['new_library']['max_age'] or 0)
            else:
                raise Exception(
                    "Unsupported source list: {url}".format(url=url))

        if self.recipe['weighted_sorting']['enabled']:
            if self.config['tmdb']['api_key']:
                print(u"Getting data from TMDb to add weighted sorting...")
                item_list = self.weighted_sorting(item_list)
            else:
                print(
                    u"Warning: TMDd API key is required "
                    u"for weighted sorting")

        # Get list of items from the Plex server
        source_libraries = []
        for library_config in self.source_library_config:
            print(u"Trying to match with items from the '{}' library ".format(
                library_config['name']))
            try:
                source_library = self.plex.server.library.section(
                    library_config['name'])
            except:  # FIXME
                raise Exception("The '{}' library does not exist".format(
                    library_config['name']))

            # FIXME: Hack until a new plexapi version is released. 3.0.4?
            if 'guid' not in source_library.ALLOWED_FILTERS:
                source_library.ALLOWED_FILTERS += ('guid', )

            source_libraries.append(source_library)

        # Create a list of matching items
        matching_items = []
        missing_items = []
        matching_total = 0
        nonmatching_idx = []

        total_items = len(item_list)
        for i, item in enumerate(item_list):
            match = False
            if max_count > 0 and matching_total >= max_count:
                nonmatching_idx.append(i)
                continue
            res = []
            for source_library in source_libraries:
                lres = source_library.search(guid='imdb://' + str(item['id']))
                if not lres and item.get('tmdb_id'):
                    lres += source_library.search(guid='themoviedb://' +
                                                  str(item['tmdb_id']))
                if not lres and item.get('tvdb_id'):
                    lres += source_library.search(guid='thetvdb://' +
                                                  str(item['tvdb_id']))
                if lres:
                    res += lres
            if not res:
                missing_items.append((i, item))
                nonmatching_idx.append(i)
                continue

            for r in res:
                imdb_id = None
                tmdb_id = None
                tvdb_id = None
                if r.guid is not None and 'imdb://' in r.guid:
                    imdb_id = r.guid.split('imdb://')[1].split('?')[0]
                elif r.guid is not None and 'themoviedb://' in r.guid:
                    tmdb_id = r.guid.split('themoviedb://')[1].split('?')[0]
                elif r.guid is not None and 'thetvdb://' in r.guid:
                    tvdb_id = (r.guid.split('thetvdb://')[1].split('?')
                               [0].split('/')[0])

                if ((imdb_id and imdb_id == str(item['id']))
                        or (tmdb_id and tmdb_id == str(item['tmdb_id']))
                        or (tvdb_id and tvdb_id == str(item['tvdb_id']))):
                    if not match:
                        match = True
                        matching_total += 1
                    matching_items.append(r)

            if match:
                percent = (float(total_items - i) / float(total_items) *
                           100) - 100
                if self.recipe['new_library']['sort_title']['absolute']:
                    print('\033c')
                    print '\rMatching: %s (%d%%)\n' % ("█" *
                                                       (int(abs(percent))),
                                                       abs(percent)),
                    print(u"{} {} ({})".format(i + 1, item['title'],
                                               item['year']))

                else:
                    print('\033c')
                    print '\rMatching: %s (%d%%)\n' % ("█" *
                                                       (int(abs(percent))),
                                                       abs(percent)),
                    print(u"{} {} ({})\n".format(matching_total, item['title'],
                                                 item['year']))

            else:
                if max_count > 0 and matching_total >= max_count:
                    missing_items.append((i, item))
                    nonmatching_idx.append(i)

        if not self.recipe['new_library']['sort_title']['absolute']:
            for i in reversed(nonmatching_idx):
                del item_list[i]

        # Create symlinks for all items in your library on the trakt watched
        print('\033c')
        print(
            u"Creating symlinks for {count} matching items in the "
            u"library...".format(count=matching_total))

        try:
            if not os.path.exists(self.recipe['new_library']['folder']):
                os.mkdir(self.recipe['new_library']['folder'])
        except:
            print(u"Unable to create the new library folder "
                  u"'{folder}'.".format(
                      folder=self.recipe['new_library']['folder']))
            print(u"Exiting script.")
            return 0

        count = 0
        updated_paths = []
        new_items = []
        if self.library_type == 'movie':
            for movie in matching_items:
                for part in movie.iterParts():
                    old_path_file = part.file
                    if self.recipe['docker']['enabled']:
                        docker_mount = self.recipe['docker']['docker_mount']
                        orig_folder = self.recipe['docker']['orig_folder']
                        old_path = ntpath.dirname(old_path_file)
                        old_path = old_path.replace(docker_mount, orig_folder)
                        file_name = ntpath.basename(old_path_file)
                        orig_filename = os.path.join(old_path, file_name)
                    else:
                        old_path, file_name = os.path.split(old_path_file)

                    folder_name = ''
                    for library_config in self.source_library_config:
                        for f in library_config['folders']:
                            f = os.path.abspath(f)
                            if old_path.lower().startswith(f.lower()):
                                folder_name = os.path.relpath(old_path, f)
                                break

                        else:
                            continue

                        if folder_name == '.':
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                file_name)
                            dir = False
                        else:
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            dir = True
                            parent_path = os.path.dirname(
                                os.path.abspath(new_path))
                            if not os.path.exists(parent_path):
                                try:
                                    os.makedirs(parent_path)
                                except OSError as e:
                                    if e.errno == errno.EEXIST and \
                                            os.path.isdir(parent_path):
                                        pass
                                    else:
                                        raise
                            # Clean up old, empty directories
                            if (os.path.exists(new_path)
                                    and not os.listdir(new_path)):
                                os.rmdir(new_path)
                        if (dir and not os.path.exists(new_path)) or (
                                not dir and not os.path.isfile(new_path)):
                            try:
                                if os.name == 'nt':
                                    if dir:
                                        subprocess.call([
                                            'mklink', '/D', new_path, old_path
                                        ],
                                                        shell=True)
                                    else:
                                        subprocess.call([
                                            'mklink', new_path, old_path_file
                                        ],
                                                        shell=True)
                                else:
                                    if dir:
                                        if self.recipe['docker']['enabled']:
                                            os.system(
                                                'ln -rs "{}" "{}"'.format(
                                                    old_path, new_path))
                                        else:
                                            os.symlink(old_path, new_path)
                                    else:
                                        os.symlink(old_path_file, new_path)
                                count += 1
                                new_items.append(movie)
                                updated_paths.append(new_path)
                            except Exception as e:
                                print(u"Symlink failed for {path}: {e}".format(
                                    path=new_path, e=e))
        else:
            for tv_show in matching_items:
                done = False
                if done:
                    continue
                for episode in tv_show.episodes():
                    if done:
                        break
                    for part in episode.iterParts():
                        old_path_file = part.file
                        if self.recipe['docker']['enabled']:
                            docker_mount = self.recipe['docker'][
                                'docker_mount']
                            orig_folder = self.recipe['docker']['orig_folder']
                            old_path = ntpath.dirname(old_path_file)
                            old_path = old_path.replace(
                                docker_mount, orig_folder)
                            file_name = ntpath.basename(old_path_file)
                            orig_filename = os.path.join(old_path, file_name)
                        else:
                            old_path, file_name = os.path.split(old_path_file)
                            folder_name = ''
                        for library_config in self.source_library_config:
                            for f in library_config['folders']:
                                if old_path.lower().startswith(f.lower()):
                                    old_path = os.path.join(
                                        f,
                                        old_path.replace(f, '').strip(
                                            os.sep).split(os.sep)[0])
                                    folder_name = os.path.relpath(old_path, f)
                                    break
                            else:
                                continue

                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            if not os.path.exists(new_path):
                                try:
                                    if os.name == 'nt':
                                        subprocess.call([
                                            'mklink', '/D', new_path, old_path
                                        ],
                                                        shell=True)
                                    else:
                                        if self.recipe['docker']['enabled']:
                                            os.system(
                                                'ln -rs "{}" "{}"'.format(
                                                    old_path, new_path))
                                            #print('ln -rs "{}" "{}"'.format(old_path,new_path))
                                        else:
                                            os.symlink(old_path, new_path)
                                    count += 1
                                    new_items.append(tv_show)
                                    updated_paths.append(new_path)
                                    done = True
                                    break
                                except Exception as e:
                                    print(u"Symlink failed for {path}: {e}".
                                          format(path=new_path, e=e))
                            else:
                                done = True
                                break

        print(u"Created symlinks for {count} new items:".format(count=count))
        for item in new_items:
            print(u"{title} ({year})".format(title=item.title, year=item.year))

        # Check if the new library exists in Plex
        print(u"Creating the '{}' library in Plex...".format(
            self.recipe['new_library']['name']))
        try:
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
            print(u"Library already exists in Plex. Scanning the library...")
            new_library.update()

        except plexapi.exceptions.NotFound:
            if self.recipe['docker']['enabled']:
                self.plex.create_new_library(
                    self.recipe['new_library']['name'],
                    self.recipe['docker']['plex_folder'], self.library_type)
            else:
                self.plex.create_new_library(
                    self.recipe['new_library']['name'],
                    self.recipe['new_library']['folder'], self.library_type)
                new_library = self.plex.server.library.section(
                    self.recipe['new_library']['name'])

        # Wait for metadata to finish downloading before continuing
        print(u"Waiting for metadata to finish downloading...")
        new_library = self.plex.server.library.section(
            self.recipe['new_library']['name'])
        while new_library.refreshing:
            time.sleep(5)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])

        # Retrieve a list of items from the new library
        print(
            u"Retrieving a list of items from the '{library}' library in "
            u"Plex...".format(library=self.recipe['new_library']['name']))
        all_new_items = new_library.all()

        # Create a dictionary of {imdb_id: item}
        imdb_map = {}
        for m in all_new_items:
            imdb_id = None
            tmdb_id = None
            tvdb_id = None
            if m.guid is not None and 'imdb://' in m.guid:
                imdb_id = m.guid.split('imdb://')[1].split('?')[0]
            elif m.guid is not None and 'themoviedb://' in m.guid:
                tmdb_id = m.guid.split('themoviedb://')[1].split('?')[0]
            elif m.guid is not None and 'thetvdb://' in m.guid:
                tvdb_id = (
                    m.guid.split('thetvdb://')[1].split('?')[0].split('/')[0])
            else:
                imdb_id = None

            if imdb_id and str(imdb_id) in item_ids:
                imdb_map[imdb_id] = m
            elif tmdb_id and ('tmdb' + str(tmdb_id)) in item_ids:
                imdb_map['tmdb' + str(tmdb_id)] = m
            elif tvdb_id and ('tvdb' + str(tvdb_id)) in item_ids:
                imdb_map['tvdb' + str(tvdb_id)] = m
            elif force_imdb_id_match:
                # Only IMDB ID found for some items
                if tmdb_id:
                    imdb_id = self.tmdb.get_imdb_id(tmdb_id)
                elif tvdb_id:
                    imdb_id = self.tvdb.get_imdb_id(tvdb_id)
                if imdb_id and str(imdb_id) in item_ids:
                    imdb_map[imdb_id] = m
                else:
                    imdb_map[m.ratingKey] = m
            else:
                imdb_map[m.ratingKey] = m

        # Modify the sort titles
        if self.recipe['new_library']['sort']:
            print(u"Setting the sort titles for the '{}' library...".format(
                self.recipe['new_library']['name']))
        if self.recipe['new_library']['sort_title']['absolute']:
            for i, m in enumerate(item_list):
                item = imdb_map.pop(m['id'], None)
                if not item:
                    item = imdb_map.pop('tmdb' + str(m.get('tmdb_id', '')),
                                        None)
                if not item:
                    item = imdb_map.pop('tvdb' + str(m.get('tvdb_id', '')),
                                        None)
                if item and self.recipe['new_library']['sort']:
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i + 1, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
        else:
            i = 0
            for m in item_list:
                item = imdb_map.pop(m['id'], None)
                if not item:
                    item = imdb_map.pop('tmdb' + str(m.get('tmdb_id', '')),
                                        None)
                if not item:
                    item = imdb_map.pop('tvdb' + str(m.get('tvdb_id', '')),
                                        None)
                if item and self.recipe['new_library']['sort']:
                    i += 1
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])

        if self.recipe['new_library']['remove_from_library'] \
                or self.recipe['new_library'].get('remove_old', False):
            # Remove items from the new library which no longer qualify
            print(u"Removing symlinks for items "
                  "which no longer qualify ".format(
                      library=self.recipe['new_library']['name']))
            count = 0
            updated_paths = []
            deleted_items = []
            max_date = add_years(
                (self.recipe['new_library']['max_age'] or 0) * -1)

            if self.library_type == 'movie':
                for movie in imdb_map.values():

                    if not self.recipe['new_library']['remove_from_library']:
                        # Only remove older than max_age
                        if not self.recipe['new_library']['max_age'] or (
                                max_date < movie.originallyAvailableAt):
                            continue

                    for part in movie.iterParts():
                        old_path_file = part.file
                        if self.recipe['docker']['enabled']:
                            docker_mount = self.recipe['docker'][
                                'docker_mount']
                            orig_folder = self.recipe['docker']['orig_folder']
                            old_path = ntpath.dirname(old_path_file)
                            old_path = old_path.replace(
                                docker_mount, orig_folder)
                            file_name = ntpath.basename(old_path_file)
                            orig_filename = os.path.join(old_path, file_name)
                        else:
                            old_path, file_name = os.path.split(old_path_file)

                        folder_name = os.path.relpath(
                            old_path, self.recipe['new_library']['folder'])

                        if folder_name == '.':
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                file_name)
                            dir = False
                        else:
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            dir = True

                        if (dir and os.path.exists(new_path)) or \
                                (not dir and os.path.isfile(new_path)):
                            try:
                                if os.name == 'nt':
                                    # Python 3.2+ only
                                    if sys.version_info < (3, 2):
                                        assert os.path.islink(new_path)
                                    if dir:
                                        os.rmdir(new_path)
                                    else:
                                        os.remove(new_path)
                                else:
                                    print(new_path)
                                    assert os.path.islink(new_path)
                                    os.unlink(new_path)
                                count += 1
                                deleted_items.append(movie)
                                updated_paths.append(new_path)
                            except Exception as e:
                                print(
                                    u"Remove symlink failed for "
                                    "{path}: {e}".format(path=new_path, e=e))
            else:
                for tv_show in imdb_map.values():
                    done = False
                    if done:
                        continue
                    for episode in tv_show.episodes():
                        if done:
                            break
                        for part in episode.iterParts():
                            if done:
                                break
                            old_path_file = part.file
                            old_path, file_name = os.path.split(old_path_file)

                            folder_name = ''
                            new_library_folder = \
                                self.recipe['new_library']['folder']
                            old_path = os.path.join(
                                new_library_folder,
                                old_path.replace(new_library_folder, '').strip(
                                    os.sep).split(os.sep)[0])
                            folder_name = os.path.relpath(
                                old_path, new_library_folder)

                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            if os.path.exists(new_path):
                                try:
                                    if os.name == 'nt':
                                        # Python 3.2+ only
                                        if sys.version_info < (3, 2):
                                            assert os.path.islink(new_path)
                                        os.rmdir(new_path)
                                    else:
                                        assert os.path.islink(new_path)
                                        os.unlink(new_path)
                                    count += 1
                                    deleted_items.append(tv_show)
                                    updated_paths.append(new_path)
                                    done = True
                                    break
                                except Exception as e:
                                    print(
                                        u"Remove symlink failed for "
                                        "{path}: {e}".format(path=new_path,
                                                             e=e))
                            else:
                                done = True
                                break

            print(u"Removed symlinks for {count} items.".format(count=count))
            for item in deleted_items:
                print(u"{title} ({year})".format(title=item.title,
                                                 year=item.year))

            # Scan the library to clean up the deleted items
            print(u"Scanning the '{library}' library...".format(
                library=self.recipe['new_library']['name']))
            new_library.update()
            time.sleep(10)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
            while new_library.refreshing:
                time.sleep(5)
                new_library = self.plex.server.library.section(
                    self.recipe['new_library']['name'])
            new_library.emptyTrash()
            all_new_items = new_library.all()
        else:
            while imdb_map:
                imdb_id, item = imdb_map.popitem()
                i += 1
                self.plex.set_sort_title(
                    new_library.key, item.ratingKey, i, item.title,
                    self.library_type,
                    self.recipe['new_library']['sort_title']['format'],
                    self.recipe['new_library']['sort_title']['visible'])

        return missing_items, len(all_new_items)

    def _run_sort_only(self):
        item_list = []
        item_ids = []
        force_imdb_id_match = False

        # Get the trakt lists
        max_count = self.recipe['new_library']['max_count']
        # Get the trakt lists
        for url in self.recipe['source_list_urls']:
            netloc = urlparse(url).netloc
            if 'api.trakt.tv' in netloc:
                (item_list, item_ids) = self.trakt.add_items(
                    self.library_type, url, item_list, item_ids,
                    self.recipe['new_library']['max_age'] or 0)
            elif not 'api.trakt.tv' in netloc:
                data = urlparse(url).path.split("/")
                if max_count > 0:
                    url = "https://api.trakt.tv/users/{}/lists/{}/items/{}?limit={}".format(
                        data[2], data[4],
                        self.library_type.replace('tv', 'shows'), max_count)
                    (item_list, item_ids) = self.trakt.add_items(
                        self.library_type, url, item_list, item_ids,
                        self.recipe['new_library']['max_age'] or 0)
                else:
                    url = "https://api.trakt.tv/users/{}/lists/{}/items/{}".format(
                        data[2], data[4],
                        self.library_type.replace('tv', 'shows'))
                    (item_list, item_ids) = self.trakt.add_items(
                        self.library_type, url, item_list, item_ids,
                        self.recipe['new_library']['max_age'] or 0)
            else:
                raise Exception(
                    "Unsupported source list: {url}".format(url=url))

        if self.recipe['weighted_sorting']['enabled']:
            if self.config['tmdb']['api_key']:
                print(u"Getting data from TMDb to add weighted sorting...")
                item_list = self.weighted_sorting(item_list)
            else:
                print(
                    u"Warning: TMDd API key is required "
                    "for weighted sorting")

        try:
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
        except:
            raise Exception("Library '{library}' does not exist".format(
                library=self.recipe['new_library']['name']))

        new_library.update()
        # Wait for metadata to finish downloading before continuing
        print(u"Waiting for metadata to finish downloading...")
        new_library = self.plex.server.library.section(
            self.recipe['new_library']['name'])
        while new_library.refreshing:
            time.sleep(10)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])

        # Retrieve a list of items from the new library
        print(
            u"Retrieving a list of items from the '{library}' library in "
            u"Plex...".format(library=self.recipe['new_library']['name']))
        all_new_items = new_library.all()

        # Create a dictionary of {imdb_id: item}
        imdb_map = {}
        for m in all_new_items:
            imdb_id = None
            tmdb_id = None
            tvdb_id = None
            if m.guid is not None and 'imdb://' in m.guid:
                imdb_id = m.guid.split('imdb://')[1].split('?')[0]
            elif m.guid is not None and 'themoviedb://' in m.guid:
                tmdb_id = m.guid.split('themoviedb://')[1].split('?')[0]
            elif m.guid is not None and 'thetvdb://' in m.guid:
                tvdb_id = (
                    m.guid.split('thetvdb://')[1].split('?')[0].split('/')[0])
            else:
                imdb_id = None

            if imdb_id and str(imdb_id) in item_ids:
                imdb_map[imdb_id] = m
            elif tmdb_id and ('tmdb' + str(tmdb_id)) in item_ids:
                imdb_map['tmdb' + str(tmdb_id)] = m
            elif tvdb_id and ('tvdb' + str(tvdb_id)) in item_ids:
                imdb_map['tvdb' + str(tvdb_id)] = m
            elif force_imdb_id_match:
                # Only IMDB ID found for some items
                if tmdb_id:
                    imdb_id = self.tmdb.get_imdb_id(tmdb_id)
                elif tvdb_id:
                    imdb_id = self.tvdb.get_imdb_id(tvdb_id)
                if imdb_id and str(imdb_id) in item_ids:
                    imdb_map[imdb_id] = m
                else:
                    imdb_map[m.ratingKey] = m
            else:
                imdb_map[m.ratingKey] = m

        # Modify the sort titles
        print(u"Setting the sort titles for the '{}' library...".format(
            self.recipe['new_library']['name']))
        if self.recipe['new_library']['sort_title']['absolute']:
            for i, m in enumerate(item_list):
                item = imdb_map.pop(m['id'], None)
                if not item:
                    item = imdb_map.pop('tmdb' + str(m.get('tmdb_id', '')),
                                        None)
                if not item:
                    item = imdb_map.pop('tvdb' + str(m.get('tvdb_id', '')),
                                        None)
                if item:
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i + 1, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
        else:
            i = 0
            for m in item_list:
                item = imdb_map.pop(m['id'], None)
                if not item:
                    item = imdb_map.pop('tmdb' + str(m.get('tmdb_id', '')),
                                        None)
                if not item:
                    item = imdb_map.pop('tvdb' + str(m.get('tvdb_id', '')),
                                        None)
                if item:
                    i += 1
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
            while imdb_map:
                imdb_id, item = imdb_map.popitem()
                i += 1
                self.plex.set_sort_title(
                    new_library.key, item.ratingKey, i, item.title,
                    self.library_type,
                    self.recipe['new_library']['sort_title']['format'],
                    self.recipe['new_library']['sort_title']['visible'])

        return len(all_new_items)

    def run(self, sort_only=False):
        if sort_only:
            print('\033c')
            print(u"Running the recipe '{}', sorting only".format(
                self.recipe_name))
            list_count = self._run_sort_only()
            print(u"Number of items in the new library: {count}".format(
                count=list_count))
        else:
            print('\033c')
            #Remove old Missing txt file
            filepath = os.path.join(
                os.getcwd() + "/Missing",
                "Missing {}.txt".format(self.recipe['new_library']['name']))
            if os.path.exists(filepath): os.remove(filepath)
            print(u"Running the recipe '{}'".format(self.recipe_name))
            missing_items, list_count = self._run()
            print(u"Number of items in the new library: {count}".format(
                count=list_count))
            print(u"Number of missing items: {count}".format(
                count=len(missing_items)))
            for idx, item in missing_items:
                print(u"{idx}\t{release}\t{imdb_id}\t{title} ({year})".format(
                    idx=idx + 1,
                    release=item.get('release_date', ''),
                    imdb_id=item['id'],
                    title=item['title'],
                    year=item['year']))

                # Add Missing movies to text File
                path = os.getcwd() + "/Missing"
                if not os.path.exists(path): os.makedirs(path)
                textname = "Missing {}.txt".format(
                    self.recipe['new_library']['name'])
                f = open(os.path.join(path, textname), "a")
                f.write("{title} ({year})\n".format(title=item['title'],
                                                    year=item['year']))
                f.close()
                # Add movies to radarr
                if self.library_type == 'movie':
                    if self.config['radarr']['add_to_radarr'] == 'true':
                        radarr.add_movie(item['id'], item['title'])
                        time.sleep(0.5)
                        print('\033c')
                else:
                    if self.config['sonarr']['add_to_sonarr'] == 'true':
                        sonarr.add_show(item['id'], item['title'])
                        time.sleep(0.5)
                        print('\033c')

    def weighted_sorting(self, item_list):
        def _get_non_theatrical_release(release_dates):
            # Returns earliest release date that is not theatrical
            # TODO PREDB
            types = {}
            for country in release_dates.get('results', []):
                # FIXME Look at others too?
                if country['iso_3166_1'] != 'US':
                    continue
                for d in country['release_dates']:
                    if d['type'] in (4, 5, 6):
                        # 4: Digital, 5: Physical, 6: TV
                        types[str(d['type'])] = datetime.datetime.strptime(
                            d['release_date'], '%Y-%m-%dT%H:%M:%S.%fZ').date()
                break

            release_date = None
            for t, d in types.items():
                if not release_date or d < release_date:
                    release_date = d

            return release_date

        def _get_age_weight(days):
            if self.library_type == 'movie':
                # Everything younger than this will get 1
                min_days = 180
                # Everything older than this will get 0
                max_days = (float(self.recipe['new_library']['max_age']) /
                            4.0 * 365.25 or 360)
            else:
                min_days = 14
                max_days = (float(self.recipe['new_library']['max_age']) /
                            4.0 * 365.25 or 180)
            if days <= min_days:
                return 1
            elif days >= max_days:
                return 0
            else:
                return 1 - (days - min_days) / (max_days - min_days)

        total_items = len(item_list)

        weights = self.recipe['weighted_sorting']['weights']

        # TMDB details
        today = datetime.date.today()
        total_tmdb_vote = 0.0
        tmdb_votes = []
        for i, m in enumerate(item_list):
            m['original_idx'] = i + 1
            details = self.tmdb.get_details(m['tmdb_id'], self.library_type)
            if not details:
                print(u"Warning: No TMDb data for {}".format(m['title']))
                continue
            m['tmdb_popularity'] = float(details['popularity'])
            m['tmdb_vote'] = float(details['vote_average'])
            m['tmdb_vote_count'] = int(details['vote_count'])
            if self.library_type == 'movie':
                if self.recipe['weighted_sorting']['better_release_date']:
                    m['release_date'] = _get_non_theatrical_release(
                        details['release_dates']) or \
                                        datetime.datetime.strptime(
                                            details['release_date'],
                                            '%Y-%m-%d').date()
                else:
                    m['release_date'] = datetime.datetime.strptime(
                        details['release_date'], '%Y-%m-%d').date()
                item_age_td = today - m['release_date']
            elif self.library_type == 'tv':
                m['last_air_date'] = datetime.datetime.strptime(
                    details['last_air_date'], '%Y-%m-%d').date()
                item_age_td = today - m['last_air_date']
            m['genres'] = [g['name'].lower() for g in details['genres']]
            m['age'] = item_age_td.days
            if (self.library_type == 'tv' or m['tmdb_vote_count'] > 150
                    or m['age'] > 50):
                tmdb_votes.append(m['tmdb_vote'])
            total_tmdb_vote += m['tmdb_vote']
            item_list[i] = m

        tmdb_votes.sort()

        for i, m in enumerate(item_list):
            # Distribute all weights evenly from 0 to 1 (times global factor)
            # More weight means it'll go higher in the final list
            index_weight = float(total_items - i) / float(total_items)
            m['index_weight'] = index_weight * weights['index']
            if m.get('tmdb_popularity'):
                if (self.library_type == 'tv' or m.get('tmdb_vote_count') > 150
                        or m['age'] > 50):
                    vote_weight = ((tmdb_votes.index(m['tmdb_vote']) + 1) /
                                   float(len(tmdb_votes)))
                else:
                    # Assume below average rating for new/less voted items
                    vote_weight = 0.25
                age_weight = _get_age_weight(float(m['age']))

                if weights.get('random'):
                    random_weight = random.random()
                    m['random_weight'] = random_weight * weights['random']
                else:
                    m['random_weight'] = 0.0

                m['vote_weight'] = vote_weight * weights['vote']
                m['age_weight'] = age_weight * weights['age']

                weight = (m['index_weight'] + m['vote_weight'] +
                          m['age_weight'] + m['random_weight'])
                for genre, value in weights['genre_bias'].items():
                    if genre.lower() in m['genres']:
                        weight *= value

                m['weight'] = weight
            else:
                m['vote_weight'] = 0.0
                m['age_weight'] = 0.0
                m['weight'] = index_weight
            item_list[i] = m

        item_list.sort(key=lambda m: m['weight'], reverse=True)

        for i, m in enumerate(item_list):
            if (i + 1) < m['original_idx']:
                net = Colors.GREEN + u'↑'
            elif (i + 1) > m['original_idx']:
                net = Colors.RED + u'↓'
            else:
                net = u' '
            net += str(abs(i + 1 - m['original_idx'])).rjust(3)
            try:
                # TODO
                print(
                    u"{} {:>3}: trnd:{:>3}, w_trnd:{:0<5}; vote:{}, "
                    "w_vote:{:0<5}; age:{:>4}, w_age:{:0<5}; w_rnd:{:0<5}; "
                    "w_cmb:{:0<5}; {} {}{}".format(
                        net, i + 1, m['original_idx'],
                        round(m['index_weight'], 3), m.get('tmdb_vote', 0.0),
                        round(m['vote_weight'], 3), m.get('age', 0),
                        round(m['age_weight'], 3),
                        round(m.get('random_weight', 0), 3),
                        round(m['weight'], 3), str(m['title']), str(m['year']),
                        Colors.RESET))
            except UnicodeEncodeError:
                pass

        return item_list
示例#2
0
class Recipe():
    plex = None
    trakt = None
    tmdb = None
    tvdb = None

    def __init__(self,
                 recipe_name,
                 sort_only=False,
                 config_file=None,
                 use_playlists=False):
        self.recipe_name = recipe_name
        self.use_playlists = use_playlists

        self.config = ConfigParser(config_file)
        self.recipe = RecipeParser(recipe_name)

        if not self.config.validate():
            raise Exception("Error(s) in config")

        if not self.recipe.validate(use_playlists=use_playlists):
            raise Exception("Error(s) in recipe")

        if self.recipe['library_type'].lower().startswith('movie'):
            self.library_type = 'movie'
        elif self.recipe['library_type'].lower().startswith('tv'):
            self.library_type = 'tv'
        else:
            raise Exception("Library type should be 'movie' or 'tv'")

        self.source_library_config = self.recipe['source_libraries']

        self.plex = plexutils.Plex(self.config['plex']['baseurl'],
                                   self.config['plex']['token'])

        if self.config['trakt']['username']:
            self.trakt = traktutils.Trakt(
                self.config['trakt']['username'],
                client_id=self.config['trakt']['client_id'],
                client_secret=self.config['trakt']['client_secret'],
                oauth_token=self.config['trakt'].get('oauth_token', ''),
                oauth=self.recipe.get('trakt_oauth', False),
                config=self.config)
            if self.trakt.oauth_token:
                self.config['trakt']['oauth_token'] = self.trakt.oauth_token

        if self.config['tmdb']['api_key']:
            self.tmdb = tmdb.TMDb(self.config['tmdb']['api_key'],
                                  cache_file=self.config['tmdb']['cache_file'])

        if self.config['tvdb']['username']:
            self.tvdb = tvdb.TheTVDB(self.config['tvdb']['username'],
                                     self.config['tvdb']['api_key'],
                                     self.config['tvdb']['user_key'])

        self.imdb = imdbutils.IMDb(self.tmdb, self.tvdb)

        self.source_map = IdMap(matching_only=True,
                                cache_file=self.config.get('guid_cache_file'))
        self.dest_map = IdMap(cache_file=self.config.get('guid_cache_file'))

    def _get_trakt_lists(self):
        item_list = []  # TODO Replace with dict, scrap item_ids?
        item_ids = []

        for url in self.recipe['source_list_urls']:
            max_age = (self.recipe['new_playlist'].get('max_age', 0) if
                       self.use_playlists else self.recipe['new_library'].get(
                           'max_age', 0))
            if 'api.trakt.tv' in url:
                (item_list,
                 item_ids) = self.trakt.add_items(self.library_type, url,
                                                  item_list, item_ids, max_age
                                                  or 0)
            elif 'imdb.com/chart' in url:
                (item_list,
                 item_ids) = self.imdb.add_items(self.library_type, url,
                                                 item_list, item_ids, max_age
                                                 or 0)
            else:
                raise Exception(
                    "Unsupported source list: {url}".format(url=url))

        if self.recipe['weighted_sorting']['enabled']:
            if self.config['tmdb']['api_key']:
                logs.info(u"Getting data from TMDb to add weighted sorting...")
                item_list = self.weighted_sorting(item_list)
            else:
                logs.warning(u"Warning: TMDd API key is required "
                             u"for weighted sorting")
        return item_list, item_ids

    def _get_plex_libraries(self):
        source_libraries = []
        for library_config in self.source_library_config:
            logs.info(
                u"Trying to match with items from the '{}' library ".format(
                    library_config['name']))
            try:
                source_library = self.plex.server.library.section(
                    library_config['name'])
            except:  # FIXME
                raise Exception("The '{}' library does not exist".format(
                    library_config['name']))

            source_libraries.append(source_library)
        return source_libraries

    def _get_matching_items(self, source_libraries, item_list):
        matching_items = []
        missing_items = []
        matching_total = 0
        nonmatching_idx = []
        max_count = (self.recipe['new_playlist'].get('max_count', 0)
                     if self.use_playlists else self.recipe['new_library'].get(
                         'max_count', 0))

        for i, item in enumerate(item_list):
            if 0 < max_count <= matching_total:
                nonmatching_idx.append(i)
                continue
            res = self.source_map.get(item.get('id'), item.get('tmdb_id'),
                                      item.get('tvdb_id'))

            if not res:
                missing_items.append((i, item))
                nonmatching_idx.append(i)
                continue

            matching_total += 1
            matching_items += res

            if not self.use_playlists and self.recipe['new_library'][
                    'sort_title']['absolute']:
                logs.info(u"{} {} ({})".format(i + 1, item['title'],
                                               item['year']))
            else:
                logs.info(u"{} {} ({})".format(matching_total, item['title'],
                                               item['year']))

        if not self.use_playlists and not self.recipe['new_library'][
                'sort_title']['absolute']:
            for i in reversed(nonmatching_idx):
                del item_list[i]

        return matching_items, missing_items, matching_total, nonmatching_idx, max_count

    def _create_symbolic_links(self, matching_items, matching_total):
        logs.info(u"Creating symlinks for {count} matching items in the "
                  u"library...".format(count=matching_total))

        try:
            if not os.path.exists(self.recipe['new_library']['folder']):
                os.mkdir(self.recipe['new_library']['folder'])
        except:
            logs.error(u"Unable to create the new library folder "
                       u"'{folder}'.".format(
                           folder=self.recipe['new_library']['folder']))
            logs.info(u"Exiting script.")
            return 0

        count = 0
        updated_paths = []
        new_items = []
        if self.library_type == 'movie':
            for movie in matching_items:
                for part in movie.iterParts():
                    old_path_file = part.file
                    old_path, file_name = os.path.split(old_path_file)

                    folder_name = ''
                    for library_config in self.source_library_config:
                        for f in self.plex.get_library_paths(
                                library_name=library_config['name']):
                            f = os.path.abspath(f)
                            if old_path.lower().startswith(f.lower()):
                                folder_name = os.path.relpath(old_path, f)
                                break
                        else:
                            continue

                        if folder_name == '.':
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                file_name)
                            dir = False
                        else:
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            dir = True
                            parent_path = os.path.dirname(
                                os.path.abspath(new_path))
                            if not os.path.exists(parent_path):
                                try:
                                    os.makedirs(parent_path)
                                except OSError as e:
                                    if e.errno == errno.EEXIST \
                                            and os.path.isdir(parent_path):
                                        pass
                                    else:
                                        raise
                            # Clean up old, empty directories
                            if os.path.exists(new_path) \
                                    and not os.listdir(new_path):
                                os.rmdir(new_path)

                        if (dir and not os.path.exists(new_path)) \
                                or not dir and not os.path.isfile(new_path):
                            try:
                                if os.name == 'nt':
                                    if dir:
                                        subprocess.call([
                                            'mklink', '/D', new_path, old_path
                                        ],
                                                        shell=True)
                                    else:
                                        subprocess.call([
                                            'mklink', new_path, old_path_file
                                        ],
                                                        shell=True)
                                else:
                                    if dir:
                                        os.symlink(old_path, new_path)
                                    else:
                                        os.symlink(old_path_file, new_path)
                                count += 1
                                new_items.append(movie)
                                updated_paths.append(new_path)
                            except Exception as e:
                                logs.error(
                                    u"Symlink failed for {path}: {e}".format(
                                        path=new_path, e=e))
        else:
            for tv_show in matching_items:
                done = False
                if done:
                    continue
                for episode in tv_show.episodes():
                    if done:
                        break
                    for part in episode.iterParts():
                        old_path_file = part.file
                        old_path, file_name = os.path.split(old_path_file)

                        folder_name = ''
                        for library_config in self.source_library_config:
                            for f in self.plex.get_library_paths(
                                    library_name=library_config['name']):
                                if old_path.lower().startswith(f.lower()):
                                    old_path = os.path.join(
                                        f,
                                        old_path.replace(f, '').strip(
                                            os.sep).split(os.sep)[0])
                                    folder_name = os.path.relpath(old_path, f)
                                    break
                            else:
                                continue

                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)

                            if not os.path.exists(new_path):
                                try:
                                    if os.name == 'nt':
                                        subprocess.call([
                                            'mklink', '/D', new_path, old_path
                                        ],
                                                        shell=True)
                                    else:
                                        os.symlink(old_path, new_path)
                                    count += 1
                                    new_items.append(tv_show)
                                    updated_paths.append(new_path)
                                    done = True
                                    break
                                except Exception as e:
                                    logs.error(
                                        u"Symlink failed for {path}: {e}".
                                        format(path=new_path, e=e))
                            else:
                                done = True
                                break

        logs.info(
            u"Created symlinks for {count} new items:".format(count=count))
        for item in new_items:
            logs.info(u"{title} ({year})".format(title=item.title,
                                                 year=getattr(
                                                     item, 'year', None)))

    def _verify_new_library_and_get_items(self, create_if_not_found=False):
        # Check if the new library exists in Plex
        try:
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
            logs.info(
                u"Library already exists in Plex. Scanning the library...")

            new_library.update()
        except plexapi.exceptions.NotFound:
            if create_if_not_found:
                self.plex.create_new_library(
                    self.recipe['new_library']['name'],
                    self.recipe['new_library']['folder'], self.library_type)
                new_library = self.plex.server.library.section(
                    self.recipe['new_library']['name'])
            else:
                raise Exception("Library '{library}' does not exist".format(
                    library=self.recipe['new_library']['name']))

        # Wait for metadata to finish downloading before continuing
        logs.info(u"Waiting for metadata to finish downloading...")
        new_library = self.plex.server.library.section(
            self.recipe['new_library']['name'])
        while new_library.refreshing:
            time.sleep(5)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])

        # Retrieve a list of items from the new library
        logs.info(
            u"Retrieving a list of items from the '{library}' library in "
            u"Plex...".format(library=self.recipe['new_library']['name']))
        return new_library, new_library.all()

    def _modify_sort_titles_and_cleanup(self,
                                        item_list,
                                        new_library,
                                        sort_only=False):
        if self.recipe['new_library']['sort']:
            logs.info(u"Setting the sort titles for the '{}' library".format(
                self.recipe['new_library']['name']))
        if self.recipe['new_library']['sort_title']['absolute']:
            for i, m in enumerate(item_list):
                item = self.dest_map.pop(m.get('id'), m.get('tmdb_id'),
                                         m.get('tvdb_id'))
                if item and self.recipe['new_library']['sort']:
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i + 1, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
        else:
            i = 0
            for m in item_list:
                i += 1
                item = self.dest_map.pop(m.get('id'), m.get('tmdb_id'),
                                         m.get('tvdb_id'))
                if item and self.recipe['new_library']['sort']:
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
        unmatched_items = list(self.dest_map.items)
        if not sort_only and (self.recipe['new_library']['remove_from_library']
                              or self.recipe['new_library'].get(
                                  'remove_old', False)):
            # Remove old items that no longer qualify
            self._remove_old_items_from_library(unmatched_items)
        elif sort_only:
            return True
        if self.recipe['new_library']['sort'] and \
                not self.recipe['new_library']['remove_from_library']:
            unmatched_items.sort(key=lambda x: x.titleSort)
            while unmatched_items:
                item = unmatched_items.pop(0)
                i += 1
                logs.info(u"{} {} ({})".format(i, item.title, item.year))
                self.plex.set_sort_title(
                    new_library.key, item.ratingKey, i, item.title,
                    self.library_type,
                    self.recipe['new_library']['sort_title']['format'],
                    self.recipe['new_library']['sort_title']['visible'])
        all_new_items = self._cleanup_new_library(new_library=new_library)
        return all_new_items

    def _remove_old_items_from_library(self, unmatched_items):
        logs.info(u"Removing symlinks for items "
                  "which no longer qualify ".format(
                      library=self.recipe['new_library']['name']))
        count = 0
        updated_paths = []
        deleted_items = []
        max_date = add_years((self.recipe['new_library']['max_age'] or 0) * -1)
        if self.library_type == 'movie':
            for movie in unmatched_items:
                if not self.recipe['new_library']['remove_from_library']:
                    # Only remove older than max_age
                    if not self.recipe['new_library']['max_age'] \
                            or (movie.originallyAvailableAt and
                                max_date < movie.originallyAvailableAt):
                        continue

                for part in movie.iterParts():
                    old_path_file = part.file
                    old_path, file_name = os.path.split(old_path_file)

                    folder_name = os.path.relpath(
                        old_path, self.recipe['new_library']['folder'])

                    if folder_name == '.':
                        new_path = os.path.join(
                            self.recipe['new_library']['folder'], file_name)
                        dir = False
                    else:
                        new_path = os.path.join(
                            self.recipe['new_library']['folder'], folder_name)
                        dir = True

                    if (dir and os.path.exists(new_path)) or (
                            not dir and os.path.isfile(new_path)):
                        try:
                            if os.name == 'nt':
                                # Python 3.2+ only
                                if sys.version_info < (3, 2):
                                    assert os.path.islink(new_path)
                                if dir:
                                    os.rmdir(new_path)
                                else:
                                    os.remove(new_path)
                            else:
                                assert os.path.islink(new_path)
                                os.unlink(new_path)
                            count += 1
                            deleted_items.append(movie)
                            updated_paths.append(new_path)
                        except Exception as e:
                            logs.error(u"Remove symlink failed for "
                                       "{path}: {e}".format(path=new_path,
                                                            e=e))
        else:
            for tv_show in unmatched_items:
                done = False
                if done:
                    continue
                for episode in tv_show.episodes():
                    if done:
                        break
                    for part in episode.iterParts():
                        if done:
                            break
                        old_path_file = part.file
                        old_path, file_name = os.path.split(old_path_file)

                        folder_name = ''
                        new_library_folder = \
                            self.recipe['new_library']['folder']
                        old_path = os.path.join(
                            new_library_folder,
                            old_path.replace(new_library_folder, '').strip(
                                os.sep).split(os.sep)[0])
                        folder_name = os.path.relpath(old_path,
                                                      new_library_folder)

                        new_path = os.path.join(
                            self.recipe['new_library']['folder'], folder_name)
                        if os.path.exists(new_path):
                            try:
                                if os.name == 'nt':
                                    # Python 3.2+ only
                                    if sys.version_info < (3, 2):
                                        assert os.path.islink(new_path)
                                    os.rmdir(new_path)
                                else:
                                    assert os.path.islink(new_path)
                                    os.unlink(new_path)
                                count += 1
                                deleted_items.append(tv_show)
                                updated_paths.append(new_path)
                                done = True
                                break
                            except Exception as e:
                                logs.error(u"Remove symlink failed for "
                                           "{path}: {e}".format(path=new_path,
                                                                e=e))
                        else:
                            done = True
                            break

        logs.info(u"Removed symlinks for {count} items.".format(count=count))
        for item in deleted_items:
            logs.info(u"{title} ({year})".format(title=item.title,
                                                 year=item.year))

    def _cleanup_new_library(self, new_library):
        # Scan the library to clean up the deleted items
        logs.info(u"Scanning the '{library}' library...".format(
            library=self.recipe['new_library']['name']))
        new_library.update()
        time.sleep(10)
        new_library = self.plex.server.library.section(
            self.recipe['new_library']['name'])
        while new_library.refreshing:
            time.sleep(5)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
        new_library.emptyTrash()
        return new_library.all()

    def _run(self, share_playlist_to_all=False):
        # Get the trakt lists
        item_list, item_ids = self._get_trakt_lists()
        force_imdb_id_match = False

        # Get list of items from the Plex server
        source_libraries = self._get_plex_libraries()

        # Populate source library guid map
        for item in item_list:
            if item.get('id'):
                self.source_map.match_imdb.append(item['id'])
            if item.get('tmdb_id'):
                self.source_map.match_tmdb.append(item['tmdb_id'])
            if item.get('tvdb_id'):
                self.source_map.match_tvdb.append(item['tvdb_id'])
        self.source_map.add_libraries(source_libraries)

        # Create a list of matching items
        matching_items, missing_items, matching_total, nonmatching_idx, max_count = self._get_matching_items(
            source_libraries, item_list)

        if self.use_playlists:
            # Start playlist process
            if self.recipe['new_playlist'][
                    'remove_from_playlist'] or self.recipe['new_playlist'].get(
                        'remove_old', False):
                # Start playlist over again
                self.plex.reset_playlist(
                    playlist_name=self.recipe['new_playlist']['name'],
                    new_items=matching_items,
                    user_names=self.recipe['new_playlist'].get(
                        'share_to_users', []),
                    all_users=(share_playlist_to_all if share_playlist_to_all
                               else self.recipe['new_playlist'].get(
                                   'share_to_all', False)))
            else:
                # Keep existing items
                self.plex.add_to_playlist_for_users(
                    playlist_name=self.recipe['new_playlist']['name'],
                    items=matching_items,
                    user_names=self.recipe['new_playlist'].get(
                        'share_to_users', []),
                    all_users=(share_playlist_to_all if share_playlist_to_all
                               else self.recipe['new_playlist'].get(
                                   'share_to_all', False)))
            playlist_items = self.plex.get_playlist_items(
                playlist_name=self.recipe['new_playlist']['name'])
            return missing_items, (len(playlist_items)
                                   if playlist_items else 0)
        else:
            # Start library process
            # Create symlinks for all items in your library on the trakt watched
            self._create_symbolic_links(matching_items=matching_items,
                                        matching_total=matching_total)
            # Post-process new library
            logs.info(u"Creating the '{}' library in Plex...".format(
                self.recipe['new_library']['name']))
            new_library, all_new_items = self._verify_new_library_and_get_items(
                create_if_not_found=True)
            self.dest_map.add_items(all_new_items)
            # Modify the sort titles
            all_new_items = self._modify_sort_titles_and_cleanup(
                item_list, new_library, sort_only=False)
            return missing_items, len(all_new_items)

    def _run_sort_only(self):
        item_list, item_ids = self._get_trakt_lists()
        force_imdb_id_match = False

        # Get existing library and its items
        new_library, all_new_items = self._verify_new_library_and_get_items(
            create_if_not_found=False)
        self.dest_map.add_items(all_new_items)
        # Modify the sort titles
        self._modify_sort_titles_and_cleanup(item_list,
                                             new_library,
                                             sort_only=True)
        return len(all_new_items)

    def run(self, sort_only=False, share_playlist_to_all=False):
        if sort_only:
            logs.info(u"Running the recipe '{}', sorting only".format(
                self.recipe_name))
            list_count = self._run_sort_only()
            logs.info(
                u"Number of items in the new {library_or_playlist}: {count}".
                format(count=list_count,
                       library_or_playlist=('playlist' if self.use_playlists
                                            else 'library')))
        else:
            logs.info(u"Running the recipe '{}'".format(self.recipe_name))
            missing_items, list_count = self._run(
                share_playlist_to_all=share_playlist_to_all)
            logs.info(
                u"Number of items in the new {library_or_playlist}: {count}".
                format(count=list_count,
                       library_or_playlist=('playlist' if self.use_playlists
                                            else 'library')))
            logs.info(u"Number of missing items: {count}".format(
                count=len(missing_items)))
            for idx, item in missing_items:
                logs.info(
                    u"{idx}\t{release}\t{imdb_id}\t{title} ({year})".format(
                        idx=idx + 1,
                        release=item.get('release_date', ''),
                        imdb_id=item['id'],
                        title=item['title'],
                        year=item['year']))

    def weighted_sorting(self, item_list):
        def _get_non_theatrical_release(release_dates):
            # Returns earliest release date that is not theatrical
            # TODO PREDB
            types = {}
            for country in release_dates.get('results', []):
                # FIXME Look at others too?
                if country['iso_3166_1'] != 'US':
                    continue
                for d in country['release_dates']:
                    if d['type'] in (4, 5, 6):
                        # 4: Digital, 5: Physical, 6: TV
                        types[str(d['type'])] = datetime.datetime.strptime(
                            d['release_date'], '%Y-%m-%dT%H:%M:%S.%fZ').date()
                break

            release_date = None
            for t, d in types.items():
                if not release_date or d < release_date:
                    release_date = d

            return release_date

        def _get_age_weight(days):
            if self.library_type == 'movie':
                # Everything younger than this will get 1
                min_days = 180
                # Everything older than this will get 0
                max_days = (float(self.recipe['new_library']['max_age']) /
                            4.0 * 365.25 or 360)
            else:
                min_days = 14
                max_days = (float(self.recipe['new_library']['max_age']) /
                            4.0 * 365.25 or 180)
            if days <= min_days:
                return 1
            elif days >= max_days:
                return 0
            else:
                return 1 - (days - min_days) / (max_days - min_days)

        total_items = len(item_list)

        weights = self.recipe['weighted_sorting']['weights']

        # TMDB details
        today = datetime.date.today()
        total_tmdb_vote = 0.0
        tmdb_votes = []
        for i, m in enumerate(item_list):
            m['original_idx'] = i + 1
            details = self.tmdb.get_details(m['tmdb_id'], self.library_type)
            if not details:
                logs.warning(u"Warning: No TMDb data for {}".format(
                    m['title']))
                continue
            m['tmdb_popularity'] = float(details['popularity'])
            m['tmdb_vote'] = float(details['vote_average'])
            m['tmdb_vote_count'] = int(details['vote_count'])
            if self.library_type == 'movie':
                if self.recipe['weighted_sorting']['better_release_date']:
                    m['release_date'] = _get_non_theatrical_release(
                        details['release_dates']) or \
                                        datetime.datetime.strptime(
                                            details['release_date'],
                                            '%Y-%m-%d').date()
                else:
                    m['release_date'] = datetime.datetime.strptime(
                        details['release_date'], '%Y-%m-%d').date()
                item_age_td = today - m['release_date']
            elif self.library_type == 'tv':
                try:
                    m['last_air_date'] = datetime.datetime.strptime(
                        details['last_air_date'], '%Y-%m-%d').date()
                except TypeError:
                    m['last_air_date'] = today
                item_age_td = today - m['last_air_date']
            m['genres'] = [g['name'].lower() for g in details['genres']]
            m['age'] = item_age_td.days
            if (self.library_type == 'tv' or m['tmdb_vote_count'] > 150
                    or m['age'] > 50):
                tmdb_votes.append(m['tmdb_vote'])
            total_tmdb_vote += m['tmdb_vote']
            item_list[i] = m

        tmdb_votes.sort()

        for i, m in enumerate(item_list):
            # Distribute all weights evenly from 0 to 1 (times global factor)
            # More weight means it'll go higher in the final list
            index_weight = float(total_items - i) / float(total_items)
            m['index_weight'] = index_weight * weights['index']
            if m.get('tmdb_popularity'):
                if (self.library_type == 'tv' or m.get('tmdb_vote_count') > 150
                        or m['age'] > 50):
                    vote_weight = ((tmdb_votes.index(m['tmdb_vote']) + 1) /
                                   float(len(tmdb_votes)))
                else:
                    # Assume below average rating for new/less voted items
                    vote_weight = 0.25
                age_weight = _get_age_weight(float(m['age']))

                if weights.get('random'):
                    random_weight = random.random()
                    m['random_weight'] = random_weight * weights['random']
                else:
                    m['random_weight'] = 0.0

                m['vote_weight'] = vote_weight * weights['vote']
                m['age_weight'] = age_weight * weights['age']

                weight = (m['index_weight'] + m['vote_weight'] +
                          m['age_weight'] + m['random_weight'])
                for genre, value in weights['genre_bias'].items():
                    if genre.lower() in m['genres']:
                        weight *= value

                m['weight'] = weight
            else:
                m['vote_weight'] = 0.0
                m['age_weight'] = 0.0
                m['weight'] = index_weight
            item_list[i] = m

        item_list.sort(key=lambda m: m['weight'], reverse=True)

        for i, m in enumerate(item_list):
            if (i + 1) < m['original_idx']:
                net = Colors.GREEN + u'↑'
            elif (i + 1) > m['original_idx']:
                net = Colors.RED + u'↓'
            else:
                net = u' '
            net += str(abs(i + 1 - m['original_idx'])).rjust(3)
            try:
                # TODO
                logs.info(
                    u"{} {:>3}: trnd:{:>3}, w_trnd:{:0<5}; vote:{}, "
                    "w_vote:{:0<5}; age:{:>4}, w_age:{:0<5}; w_rnd:{:0<5}; "
                    "w_cmb:{:0<5}; {} {}{}".format(
                        net, i + 1, m['original_idx'],
                        round(m['index_weight'], 3), m.get('tmdb_vote', 0.0),
                        round(m['vote_weight'], 3), m.get('age', 0),
                        round(m['age_weight'], 3),
                        round(m.get('random_weight', 0), 3),
                        round(m['weight'], 3), str(m['title']), str(m['year']),
                        Colors.RESET))
            except UnicodeEncodeError:
                pass

        return item_list
示例#3
0
class Recipe():
    """
    recipe class
    """
    plex = None
    trakt = None
    tmdb = None
    tvdb = None

    def __init__(self, recipe_name, config_file=None):
        self.recipe_name = recipe_name

        self.config = ConfigParser(config_file)
        self.recipe = RecipeParser(recipe_name)

        if self.recipe['library_type'].lower().startswith('movie'):
            self.library_type = 'movie'
        elif self.recipe['library_type'].lower().startswith('tv'):
            self.library_type = 'tv'
        else:
            raise Exception("Library type should be 'movie' or 'tv'")

        self.source_library_config = self.recipe['source_libraries']

        self.plex = plexutils.Plex(self.config['plex']['baseurl'],
                                   self.config['plex']['token'])

        if self.config['trakt']['username']:
            self.trakt = traktutils.Trakt(
                self.config['trakt']['username'],
                client_id=self.config['trakt']['client_id'],
                client_secret=self.config['trakt']['client_secret'],
                oauth_token=self.config['trakt'].get('oauth_token', ''),
                oauth=self.recipe.get('trakt_oauth', False),
                config=self.config)
            if self.trakt.oauth_token:
                self.config['trakt']['oauth_token'] = self.trakt.oauth_token

        if self.config['tmdb']['api_key']:
            self.tmdb = tmdb.TMDb(self.config['tmdb']['api_key'],
                                  cache_file=self.config['tmdb']['cache_file'])

        if self.config['tvdb']['username']:
            self.tvdb = tvdb.TheTVDB(self.config['tvdb']['username'],
                                     self.config['tvdb']['api_key'],
                                     self.config['tvdb']['user_key'])

        self.imdb = imdbutils.IMDb(self.tmdb, self.tvdb)

    @classmethod
    def _imdb_matches(cls, imdb_id, item):
        matches = False
        if imdb_id == str(item['id']):
            matches = True
        return matches

    @classmethod
    def _tmdb_matches(cls, tmdb_id, item):
        matches = False
        if tmdb_id == str(item['tmdb_id']):
            matches = True
        return matches

    @classmethod
    def _tvdb_matches(cls, tvdb_id, item):
        matches = False
        if tvdb_id == str(item['tvdb_id']):
            matches = True
        return matches

    def _get_source_list_urls(self):
        item_list = []
        item_ids = []

        for url in self.recipe['source_list_urls']:
            if 'api.trakt.tv' in url:
                item_list, item_ids = \
                    self.trakt.add_items( \
                        self.library_type, \
                        url, \
                        item_list, \
                        item_ids, \
                        self.recipe['new_library']['max_age'] \
                        or 0)
            elif 'imdb.com/chart' in url:
                item_list, item_ids = \
                    self.imdb.add_items( \
                    self.library_type, \
                    url, \
                    item_list, \
                    item_ids, \
                    self.recipe['new_library']['max_age'] \
                    or 0)
            else:
                raise Exception("Unsupported source list: {url}".format( \
                    url=url))

        return item_list, item_ids

    def _apply_weighted_sorting(self, item_list):
        if self.recipe['weighted_sorting']['enabled']:
            if self.config['tmdb']['api_key']:
                print("""
                    Getting data from TMDb to add weighted sorting...""")
                item_list = self.weighted_sorting(item_list)
            else:
                print("""
                    Warning: TMDd API key is required for weighted sorting""")

        return item_list

    def _get_source_libraries(self):
        source_libraries = []

        for library_config in self.source_library_config:
            print("""
                Trying to match with items from the '{}' library""".format( \
                    library_config['name']))
            try:
                source_library = self.plex.server.library.section( \
                    library_config['name'])
            except:
                raise Exception("""
                    The '{}' library does not exist""".format( \
                        library_config['name']))

            if 'guid' not in source_library.ALLOWED_FILTERS:
                source_library.ALLOWED_FILTERS += ('guid', )

            source_libraries.append(source_library)

        return source_libraries

    @classmethod
    def _get_show_results(cls, source_libraries, show):
        results = []

        for source_library in source_libraries:
            curr_result = source_library.search( \
                guid='imdb://' + str(show['id']))
            if not curr_result and show.get('tmdb_id'):
                curr_result += source_library.search( \
                    guid='themoviedb://' + str(show['tmdb_id']))
            if not curr_result and show.get('tvdb_id'):
                curr_result += source_library.search( \
                    guid='thetvdb://' + str(show['tvdb_id']))
            if curr_result:
                results += curr_result

        return results

    @classmethod
    def _get_show_ids(cls, show):
        imdb_id, tmdb_id, tvdb_id = None, None, None

        if show.guid is not None and 'imdb://' in show.guid:
            imdb_id = show.guid.split( \
                'imdb://')[1].split('?')[0]
        elif show.guid is not None and 'themoviedb://' in show.guid:
            tmdb_id = show.guid.split( \
                'themoviedb://')[1].split('?')[0]
        elif show.guid is not None and 'thetvdb://' in show.guid:
            tvdb_id = (show.guid.split( \
                'thetvdb://')[1].split('?')[0].split('/')[0])

        return imdb_id, tmdb_id, tvdb_id

    def _run(self):
        force_imdb_id_match = False

        item_list, item_ids = self._get_source_list_urls()
        item_list = self._apply_weighted_sorting(item_list)
        source_libraries = self._get_source_libraries()

        # Create a list of matching items
        matching_items = []
        missing_items = []
        matching_total = 0
        nonmatching_idx = []
        max_count = self.recipe['new_library']['max_count']

        for i, item in enumerate(item_list):
            match = False
            if max_count > 0 and matching_total >= max_count:
                nonmatching_idx.append(i)
                continue

            results = self._get_show_results(source_libraries, item)
            if not results:
                missing_items.append((i, item))
                nonmatching_idx.append(i)
                continue

            for result in results:
                imdb_id, tmdb_id, tvdb_id = self._get_show_ids(result)

                if self._imdb_matches(imdb_id, item) or \
                    self._tmdb_matches(tmdb_id, item) or \
                    self._tvdb_matches(tvdb_id, item):

                    if not match:
                        match = True
                        matching_total += 1

                    matching_items.append(result)

            if match:
                if self.recipe['new_library']['sort_title']['absolute']:
                    print(u"{} {} ({})".format(i + 1, item['title'],
                                               item['year']))
                else:
                    print(u"{} {} ({})".format(matching_total, item['title'],
                                               item['year']))
            else:
                missing_items.append((i, item))
                nonmatching_idx.append(i)

        if not self.recipe['new_library']['sort_title']['absolute']:
            for i in reversed(nonmatching_idx):
                del item_list[i]

        # Create symlinks for all items in your library on the trakt watched
        print("""
            Creating symlinks for {count} matching items in the 
            library...""".format( \
                count=matching_total))

        try:
            if not os.path.exists(self.recipe['new_library']['folder']):
                os.mkdir(self.recipe['new_library']['folder'])
        except:
            print(u"Unable to create the new library folder "
                  u"'{folder}'.".format(
                      folder=self.recipe['new_library']['folder']))
            print(u"Exiting script.")
            return 0

        count = 0
        updated_paths = []
        new_items = []
        if self.library_type == 'movie':
            for movie in matching_items:
                for part in movie.iterParts():
                    old_path_file = part.file
                    old_path, file_name = os.path.split(old_path_file)

                    folder_name = ''
                    for library_config in self.source_library_config:
                        for fldr in library_config['folders']:
                            fldr = os.path.abspath(fldr)
                            if old_path.lower().startswith(fldr.lower()):
                                folder_name = os.path.relpath(old_path, fldr)
                                break
                        else:
                            continue

                        if folder_name == '.':
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                file_name)
                            drct = False
                        else:
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            drct = True
                            parent_path = os.path.dirname(
                                os.path.abspath(new_path))
                            if not os.path.exists(parent_path):
                                try:
                                    os.makedirs(parent_path)
                                except OSError as err:
                                    if err.errno == errno.EEXIST \
                                            and os.path.isdir(parent_path):
                                        pass
                                    else:
                                        raise
                            # Clean up old, empty directories
                            if os.path.exists(new_path) \
                                    and not os.listdir(new_path):
                                os.rmdir(new_path)

                        if (drct and not os.path.exists(new_path)) \
                                or not drct and not os.path.isfile(new_path):
                            try:
                                if os.name == 'nt':
                                    if drct:
                                        subprocess.call([
                                            'mklink', '/D', new_path, old_path
                                        ],
                                                        shell=True)
                                    else:
                                        subprocess.call([
                                            'mklink', new_path, old_path_file
                                        ],
                                                        shell=True)
                                else:
                                    if drct:
                                        os.symlink(old_path, new_path)
                                    else:
                                        os.symlink(old_path_file, new_path)
                                count += 1
                                new_items.append(movie)
                                updated_paths.append(new_path)
                            except Exception as err:
                                print(u"Symlink failed for {path}: {error}".
                                      format(path=new_path, error=err))
        else:
            for tv_show in matching_items:
                done = False
                if done:
                    continue
                for episode in tv_show.episodes():
                    if done:
                        break
                    for part in episode.iterParts():
                        old_path_file = part.file
                        old_path, file_name = os.path.split(old_path_file)

                        folder_name = ''
                        for library_config in self.source_library_config:
                            for fldr in library_config['folders']:
                                if old_path.lower().startswith(fldr.lower()):
                                    old_path = \
                                        os.path.join(fldr, \
                                            old_path.replace(fldr, \
                                                '').strip(os.sep).split( \
                                                    os.sep)[0])
                                    folder_name = os.path.relpath(
                                        old_path, fldr)
                                    break
                            else:
                                continue

                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)

                            if not os.path.exists(new_path):
                                try:
                                    if os.name == 'nt':
                                        subprocess.call([
                                            'mklink', '/D', new_path, old_path
                                        ],
                                                        shell=True)
                                    else:
                                        os.symlink(old_path, new_path)
                                    count += 1
                                    new_items.append(tv_show)
                                    updated_paths.append(new_path)
                                    done = True
                                    break
                                except Exception as err:
                                    print(
                                        u"Symlink failed for {path}: {error}".
                                        format(path=new_path, error=err))
                            else:
                                done = True
                                break

        print(u"Created symlinks for {count} new items:".format(count=count))
        for item in new_items:
            print(u"{title} ({year})".format(title=item.title, year=item.year))

        # Check if the new library exists in Plex
        print(u"Creating the '{}' library in Plex...".format(
            self.recipe['new_library']['name']))
        try:
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
            print(u"Library already exists in Plex. Scanning the library...")

            new_library.update()
        except plexapi.exceptions.NotFound:
            self.plex.create_new_library(self.recipe['new_library']['name'],
                                         self.recipe['new_library']['folder'],
                                         self.library_type)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])

        # Wait for metadata to finish downloading before continuing
        print(u"Waiting for metadata to finish downloading...")
        new_library = self.plex.server.library.section(
            self.recipe['new_library']['name'])
        while new_library.refreshing:
            time.sleep(5)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])

        # Retrieve a list of items from the new library
        print(u"Retrieving a list of items from the '{library}' library in "
              u"Plex...".format(library=self.recipe['new_library']['name']))
        all_new_items = new_library.all()

        # Create a dictionary of {imdb_id: item}
        imdb_map = {}
        for mymovie in all_new_items:
            imdb_id = None
            tmdb_id = None
            tvdb_id = None
            if mymovie.guid is not None and 'imdb://' in mymovie.guid:
                imdb_id = mymovie.guid.split('imdb://')[1].split('?')[0]
            elif mymovie.guid is not None and 'themoviedb://' in mymovie.guid:
                tmdb_id = mymovie.guid.split('themoviedb://')[1].split('?')[0]
            elif mymovie.guid is not None and 'thetvdb://' in mymovie.guid:
                tvdb_id = ( \
                    mymovie.guid.split('thetvdb://')[1].split('?')[0].split('/')[0])
            else:
                imdb_id = None

            if imdb_id and str(imdb_id) in item_ids:
                imdb_map[imdb_id] = mymovie
            elif tmdb_id and ('tmdb' + str(tmdb_id)) in item_ids:
                imdb_map['tmdb' + str(tmdb_id)] = mymovie
            elif tvdb_id and ('tvdb' + str(tvdb_id)) in item_ids:
                imdb_map['tvdb' + str(tvdb_id)] = mymovie
            elif force_imdb_id_match:
                # Only IMDB ID found for some items
                if tmdb_id:
                    imdb_id = self.tmdb.get_imdb_id(tmdb_id)
                elif tvdb_id:
                    imdb_id = self.tvdb.get_imdb_id(tvdb_id)
                if imdb_id and str(imdb_id) in item_ids:
                    imdb_map[imdb_id] = mymovie
                else:
                    imdb_map[mymovie.ratingKey] = mymovie
            else:
                imdb_map[mymovie.ratingKey] = mymovie

        # Modify the sort titles
        if self.recipe['new_library']['sort']:
            print(u"Setting the sort titles for the '{}' library...".format(
                self.recipe['new_library']['name']))
        if self.recipe['new_library']['sort_title']['absolute']:
            for i, mymovie in enumerate(item_list):
                item = imdb_map.pop(mymovie['id'], None)
                if not item:
                    item = imdb_map.pop(
                        'tmdb' + str(mymovie.get('tmdb_id', '')), None)
                if not item:
                    item = imdb_map.pop(
                        'tvdb' + str(mymovie.get('tvdb_id', '')), None)
                if item and self.recipe['new_library']['sort']:
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i + 1,
                        mymovie['title'], self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
        else:
            i = 0
            for mymovie in item_list:
                item = imdb_map.pop(mymovie['id'], None)
                if not item:
                    item = imdb_map.pop(
                        'tmdb' + str(mymovie.get('tmdb_id', '')), None)
                if not item:
                    item = imdb_map.pop(
                        'tvdb' + str(mymovie.get('tvdb_id', '')), None)
                if item and self.recipe['new_library']['sort']:
                    i += 1
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i, mymovie['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])

        if self.recipe['new_library']['remove_from_library'] \
                or self.recipe['new_library'].get('remove_old', False):
            # Remove items from the new library which no longer qualify
            print(u"Removing symlinks for items "
                  "which no longer qualify ".format(
                      library=self.recipe['new_library']['name']))
            count = 0
            updated_paths = []
            deleted_items = []
            max_date = add_years(
                (self.recipe['new_library']['max_age'] or 0) * -1)
            if self.library_type == 'movie':
                for movie in imdb_map.values():
                    if not self.recipe['new_library']['remove_from_library']:
                        # Only remove older than max_age
                        if not self.recipe['new_library']['max_age'] \
                                or (max_date < movie.originallyAvailableAt):
                            continue

                    for part in movie.iterParts():
                        old_path_file = part.file
                        old_path, file_name = os.path.split(old_path_file)

                        folder_name = os.path.relpath(
                            old_path, self.recipe['new_library']['folder'])

                        if folder_name == '.':
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                file_name)
                            drct = False
                        else:
                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            drct = True

                        if (drct and os.path.exists(new_path)) or (
                                not drct and os.path.isfile(new_path)):
                            try:
                                if os.name == 'nt':
                                    # Python 3.2+ only
                                    if sys.version_info < (3, 2):
                                        assert os.path.islink(new_path)
                                    if drct:
                                        os.rmdir(new_path)
                                    else:
                                        os.remove(new_path)
                                else:
                                    assert os.path.islink(new_path)
                                    os.unlink(new_path)
                                count += 1
                                deleted_items.append(movie)
                                updated_paths.append(new_path)
                            except Exception as err:
                                print(u"Remove symlink failed for "
                                      "{path}: {error}".format(path=new_path,
                                                               error=err))
            else:
                for tv_show in imdb_map.values():
                    done = False
                    if done:
                        continue
                    for episode in tv_show.episodes():
                        if done:
                            break
                        for part in episode.iterParts():
                            if done:
                                break
                            old_path_file = part.file
                            old_path, file_name = os.path.split(old_path_file)

                            folder_name = ''
                            new_library_folder = \
                                self.recipe['new_library']['folder']
                            old_path = os.path.join(
                                new_library_folder,
                                old_path.replace(new_library_folder, '').strip(
                                    os.sep).split(os.sep)[0])
                            folder_name = os.path.relpath(
                                old_path, new_library_folder)

                            new_path = os.path.join(
                                self.recipe['new_library']['folder'],
                                folder_name)
                            if os.path.exists(new_path):
                                try:
                                    if os.name == 'nt':
                                        # Python 3.2+ only
                                        if sys.version_info < (3, 2):
                                            assert os.path.islink(new_path)
                                        os.rmdir(new_path)
                                    else:
                                        assert os.path.islink(new_path)
                                        os.unlink(new_path)
                                    count += 1
                                    deleted_items.append(tv_show)
                                    updated_paths.append(new_path)
                                    done = True
                                    break
                                except Exception as err:
                                    print(u"Remove symlink failed for "
                                          "{path}: {e}".format(path=new_path,
                                                               err=err))
                            else:
                                done = True
                                break

            print(u"Removed symlinks for {count} items.".format(count=count))
            for item in deleted_items:
                print(u"{title} ({year})".format(title=item.title,
                                                 year=item.year))

            # Scan the library to clean up the deleted items
            print(u"Scanning the '{library}' library...".format(
                library=self.recipe['new_library']['name']))
            new_library.update()
            time.sleep(10)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
            while new_library.refreshing:
                time.sleep(5)
                new_library = self.plex.server.library.section(
                    self.recipe['new_library']['name'])
            new_library.emptyTrash()
            all_new_items = new_library.all()
        else:
            while imdb_map:
                imdb_id, item = imdb_map.popitem()
                i += 1
                self.plex.set_sort_title(
                    new_library.key, item.ratingKey, i, item.title,
                    self.library_type,
                    self.recipe['new_library']['sort_title']['format'],
                    self.recipe['new_library']['sort_title']['visible'])

        return missing_items, len(all_new_items)

    def _run_sort_only(self):
        item_list = []
        item_ids = []
        force_imdb_id_match = False

        # Get the trakt lists
        for url in self.recipe['source_list_urls']:
            if 'api.trakt.tv' in url:
                (item_list, item_ids) = self.trakt.add_items(
                    self.library_type, url, item_list, item_ids,
                    self.recipe['new_library']['max_age'] or 0)
            else:
                raise Exception(
                    "Unsupported source list: {url}".format(url=url))

        if self.recipe['weighted_sorting']['enabled']:
            if self.config['tmdb']['api_key']:
                print(u"Getting data from TMDb to add weighted sorting...")
                item_list = self.weighted_sorting(item_list)
            else:
                print(u"Warning: TMDd API key is required "
                      "for weighted sorting")

        try:
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])
        except:
            raise Exception("Library '{library}' does not exist".format(
                library=self.recipe['new_library']['name']))

        new_library.update()
        # Wait for metadata to finish downloading before continuing
        print(u"Waiting for metadata to finish downloading...")
        new_library = self.plex.server.library.section(
            self.recipe['new_library']['name'])
        while new_library.refreshing:
            time.sleep(5)
            new_library = self.plex.server.library.section(
                self.recipe['new_library']['name'])

        # Retrieve a list of items from the new library
        print(u"Retrieving a list of items from the '{library}' library in "
              u"Plex...".format(library=self.recipe['new_library']['name']))
        all_new_items = new_library.all()

        # Create a dictionary of {imdb_id: item}
        imdb_map = {}
        for m in all_new_items:
            imdb_id = None
            tmdb_id = None
            tvdb_id = None
            if m.guid is not None and 'imdb://' in m.guid:
                imdb_id = m.guid.split('imdb://')[1].split('?')[0]
            elif m.guid is not None and 'themoviedb://' in m.guid:
                tmdb_id = m.guid.split('themoviedb://')[1].split('?')[0]
            elif m.guid is not None and 'thetvdb://' in m.guid:
                tvdb_id = ( \
                    m.guid.split('thetvdb://')[1].split('?')[0].split('/')[0])
            else:
                imdb_id = None

            if imdb_id and str(imdb_id) in item_ids:
                imdb_map[imdb_id] = m
            elif tmdb_id and ('tmdb' + str(tmdb_id)) in item_ids:
                imdb_map['tmdb' + str(tmdb_id)] = m
            elif tvdb_id and ('tvdb' + str(tvdb_id)) in item_ids:
                imdb_map['tvdb' + str(tvdb_id)] = m
            elif force_imdb_id_match:
                # Only IMDB ID found for some items
                if tmdb_id:
                    imdb_id = self.tmdb.get_imdb_id(tmdb_id)
                elif tvdb_id:
                    imdb_id = self.tvdb.get_imdb_id(tvdb_id)
                if imdb_id and str(imdb_id) in item_ids:
                    imdb_map[imdb_id] = m
                else:
                    imdb_map[m.ratingKey] = m
            else:
                imdb_map[m.ratingKey] = m

        # Modify the sort titles
        print(u"Setting the sort titles for the '{}' library...".format(
            self.recipe['new_library']['name']))
        if self.recipe['new_library']['sort_title']['absolute']:
            for i, m in enumerate(item_list):
                item = imdb_map.pop(m['id'], None)
                if not item:
                    item = imdb_map.pop('tmdb' + str(m.get('tmdb_id', '')),
                                        None)
                if not item:
                    item = imdb_map.pop('tvdb' + str(m.get('tvdb_id', '')),
                                        None)
                if item:
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i + 1, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
        else:
            i = 0
            for m in item_list:
                item = imdb_map.pop(m['id'], None)
                if not item:
                    item = imdb_map.pop('tmdb' + str(m.get('tmdb_id', '')),
                                        None)
                if not item:
                    item = imdb_map.pop('tvdb' + str(m.get('tvdb_id', '')),
                                        None)
                if item:
                    i += 1
                    self.plex.set_sort_title(
                        new_library.key, item.ratingKey, i, m['title'],
                        self.library_type,
                        self.recipe['new_library']['sort_title']['format'],
                        self.recipe['new_library']['sort_title']['visible'])
            while imdb_map:
                imdb_id, item = imdb_map.popitem()
                i += 1
                self.plex.set_sort_title(
                    new_library.key, item.ratingKey, i, item.title,
                    self.library_type,
                    self.recipe['new_library']['sort_title']['format'],
                    self.recipe['new_library']['sort_title']['visible'])

        return len(all_new_items)

    def run(self, sort_only=False):
        if sort_only:
            print(u"Running the recipe '{}', sorting only".format(
                self.recipe_name))
            list_count = self._run_sort_only()
            print(u"Number of items in the new library: {count}".format(
                count=list_count))
        else:
            print(u"Running the recipe '{}'".format(self.recipe_name))
            missing_items, list_count = self._run()
            print(u"Number of items in the new library: {count}".format(
                count=list_count))
            print(u"Number of missing items: {count}".format(
                count=len(missing_items)))
            for idx, item in missing_items:
                print(u"{idx}\t{release}\t{imdb_id}\t{title} ({year})".format(
                    idx=idx + 1,
                    release=item.get('release_date', ''),
                    imdb_id=item['id'],
                    title=item['title'],
                    year=item['year']))

    def weighted_sorting(self, item_list):
        def _get_non_theatrical_release(release_dates):
            # Returns earliest release date that is not theatrical
            types = {}
            for country in release_dates.get('results', []):
                if country['iso_3166_1'] != 'US':
                    continue
                for d in country['release_dates']:
                    if d['type'] in (4, 5, 6):
                        # 4: Digital, 5: Physical, 6: TV
                        types[str(d['type'])] = datetime.datetime.strptime(
                            d['release_date'], '%Y-%m-%dT%H:%M:%S.%fZ').date()
                break

            release_date = None
            for t, d in types.items():
                if not release_date or d < release_date:
                    release_date = d

            return release_date

        def _get_age_weight(days):
            if self.library_type == 'movie':
                # Everything younger than this will get 1
                min_days = 180
                # Everything older than this will get 0
                max_days = (float(self.recipe['new_library']['max_age']) /
                            4.0 * 365.25 or 360)
            else:
                min_days = 14
                max_days = (float(self.recipe['new_library']['max_age']) /
                            4.0 * 365.25 or 180)
            if days <= min_days:
                return 1
            elif days >= max_days:
                return 0
            else:
                return 1 - (days - min_days) / (max_days - min_days)

        total_items = len(item_list)

        weights = self.recipe['weighted_sorting']['weights']

        # TMDB details
        today = datetime.date.today()
        total_tmdb_vote = 0.0
        tmdb_votes = []
        for i, m in enumerate(item_list):
            m['original_idx'] = i + 1
            details = self.tmdb.get_details(m['tmdb_id'], self.library_type)
            if not details:
                print(u"Warning: No TMDb data for {}".format(m['title']))
                continue
            m['tmdb_popularity'] = float(details['popularity'])
            m['tmdb_vote'] = float(details['vote_average'])
            m['tmdb_vote_count'] = int(details['vote_count'])
            if self.library_type == 'movie':
                if self.recipe['weighted_sorting']['better_release_date']:
                    m['release_date'] = _get_non_theatrical_release(
                        details['release_dates']) or \
                                        datetime.datetime.strptime(
                                            details['release_date'],
                                            '%Y-%m-%d').date()
                else:
                    m['release_date'] = datetime.datetime.strptime(
                        details['release_date'], '%Y-%m-%d').date()
                item_age_td = today - m['release_date']
            elif self.library_type == 'tv':
                m['last_air_date'] = datetime.datetime.strptime(
                    details['last_air_date'], '%Y-%m-%d').date()
                item_age_td = today - m['last_air_date']
            m['genres'] = [g['name'].lower() for g in details['genres']]
            m['age'] = item_age_td.days
            if (self.library_type == 'tv' or m['tmdb_vote_count'] > 150
                    or m['age'] > 50):
                tmdb_votes.append(m['tmdb_vote'])
            total_tmdb_vote += m['tmdb_vote']
            item_list[i] = m

        tmdb_votes.sort()

        for i, m in enumerate(item_list):
            # Distribute all weights evenly from 0 to 1 (times global factor)
            # More weight means it'll go higher in the final list
            index_weight = float(total_items - i) / float(total_items)
            m['index_weight'] = index_weight * weights['index']
            if m.get('tmdb_popularity'):
                if (self.library_type == 'tv' or m.get('tmdb_vote_count') > 150
                        or m['age'] > 50):
                    vote_weight = ((tmdb_votes.index(m['tmdb_vote']) + 1) /
                                   float(len(tmdb_votes)))
                else:
                    # Assume below average rating for new/less voted items
                    vote_weight = 0.25
                age_weight = _get_age_weight(float(m['age']))

                if weights.get('random'):
                    random_weight = random.random()
                    m['random_weight'] = random_weight * weights['random']
                else:
                    m['random_weight'] = 0.0

                m['vote_weight'] = vote_weight * weights['vote']
                m['age_weight'] = age_weight * weights['age']

                weight = (m['index_weight'] + m['vote_weight'] +
                          m['age_weight'] + m['random_weight'])
                for genre, value in weights['genre_bias'].items():
                    if genre.lower() in m['genres']:
                        weight *= value

                m['weight'] = weight
            else:
                m['vote_weight'] = 0.0
                m['age_weight'] = 0.0
                m['weight'] = index_weight
            item_list[i] = m

        item_list.sort(key=lambda m: m['weight'], reverse=True)

        for i, m in enumerate(item_list):
            if (i + 1) < m['original_idx']:
                net = Colors.GREEN + u'↑'
            elif (i + 1) > m['original_idx']:
                net = Colors.RED + u'↓'
            else:
                net = u' '
            net += str(abs(i + 1 - m['original_idx'])).rjust(3)
            try:
                print(u"{} {:>3}: trnd:{:>3}, w_trnd:{:0<5}; vote:{}, "
                      "w_vote:{:0<5}; age:{:>4}, w_age:{:0<5}; w_rnd:{:0<5}; "
                      "w_cmb:{:0<5}; {} {}{}".format(
                          net, i + 1, m['original_idx'],
                          round(m['index_weight'], 3), m.get('tmdb_vote', 0.0),
                          round(m['vote_weight'], 3), m.get('age', 0),
                          round(m['age_weight'], 3),
                          round(m.get('random_weight', 0), 3),
                          round(m['weight'], 3), str(m['title']),
                          str(m['year']), Colors.RESET))
            except UnicodeEncodeError:
                pass

        return item_list