Пример #1
0
class BotHandler:

    #конструктор класса для работы с ботом
    def __init__(self, token):
        self.config=ConfigParser()
        self.config.read(CONFIG_FILE_NAME,"utf_8_sig")
        self.started = True;
        
        db = DbHelper()
        tables_db = db.execute_select("SELECT * FROM {}.{}".format(self.config.get("postgresql","schema"), self.config.get("postgresql","tables_dict")))

        self.tables = []
        for table in tables_db:
            self.tables.append(NotificationTable(table[1]))
                
        self.TOKEN = token
        self.URL = "https://api.telegram.org/bot{}/".format(token)
        LogDBHandler.log(Actions.start, self.config.get("botsettings","messenger_name"),"", "", "success", "")

    #получает url
    def get_url(self, url, offset=None, timeout=100):
        params = {'timeout': timeout, 'offset': offset}
        response = requests.get(url, params)
        content = response.content.decode("utf8")
        return content

    #вспомогательная функция получает json из url
    def get_json_from_url(self, url):
        content = self.get_url(url)
        js = json.loads(content)
        return js

    #получить все обновления
    def get_updates(self, offset=None, timeout=100):
        url = self.URL + "getUpdates".format(timeout)
        if offset:
            url += "?offset={}".format(offset)
        js = self.get_json_from_url(url)
        return js

    #получить ид последнего обновления
    def get_last_update_id(self, updates):
        update_ids = []
        for update in updates["result"]:
            update_ids.append(int(update["update_id"]))
        return max(update_ids)

    #получить текст и ид чата последнего сообщения
    def get_last_chat_id_and_text(self, updates):
        num_updates = len(updates["result"])
        last_update = num_updates - 1
        text = updates["result"][last_update]["message"]["text"]
        chat_id = updates["result"][last_update]["message"]["chat"]["id"]
        return (text, chat_id)

    #функция ответа на входящие сообщения
    def echo_all(self, updates):
        for update in updates["result"]:  
            try:
                print(self.config.get("commands","list_subscribers"))
                text = update["message"]["text"]
                chat = update["message"]["chat"]["id"]
                LogDBHandler.log(Actions.message, self.config.get("botsettings","messenger_name"), self.get_phone_number(chat), text, "success", "user", chat,"","{}", json.dumps(update).replace("'","\""))
                first_name = update["message"]["from"]["first_name"]
                if text=="/start" and self.started:
                    self.subscribe(first_name,chat)
                elif text==self.config.get("commands","unsubscribe") and self.started:
                    self.unsubscribe(first_name,chat)
                elif text==self.config.get("commands","disengage") and self.get_user_role(first_name,chat)=="ADMIN" and self.started==True:
                    self.started=False
                    LogDBHandler.log(Actions.stop, self.config.get("botsettings","messenger_name"), self.get_phone_number(chat), text, "success", "user", chat,"","{}", json.dumps(update).replace("'","\""))
                elif text==self.config.get("commands","engage") and self.get_user_role(first_name,chat)=="ADMIN" and self.started==False:
                    self.started=True
                    LogDBHandler.log(Actions.start, self.config.get("botsettings","messenger_name"), self.get_phone_number(chat), text, "success", "user", chat,"","{}", json.dumps(update).replace("'","\""))
                elif text==self.config.get("commands","list_subscribers") and self.get_user_role(first_name,chat)=="ADMIN":
                    self.list_users(first_name,chat)
                else:
                    self.try_process_code(first_name, chat, text)
            except KeyError:
                chat = update["message"]["chat"]["id"]
                first_name = update["message"]["from"]["first_name"]
                number = update["message"]["contact"]["phone_number"]
                self.try_process_number(first_name, chat, number)
            except Exception as e:
                LogDBHandler.log(Actions.error, self.config.get("botsettings","messenger_name"), "", str(e.message).replace("/","//").replace("'","\""), "fail", "bot", "","","{}", json.dumps(update).replace("'","\""))
                print(e)

    #вывести список пользователей
    def list_users(self,  first_name, chat_id):
        result = ""
        db=DbHelper()
        rows = db.execute_select("select user_name, phone_number, chat_id, (select caption from {0}.{1} where id=status), role from {0}.{2}"
                                 .format(
                                     self.config.get("postgresql","schema"),
                                     self.config.get("postgresql","statuse_table"),
                                     self.config.get("postgresql","users_table")))
        for row in rows:
            result+="Имя: {}, Номер: {}, Статус: {}, Роль: {}\n".format(row[0],row[1],row[3],row[4])
        self.send_message(result,chat_id)
    
    #обработка номера телефона
    def try_process_number(self,  first_name, chat_id, number):
        db = DbHelper()
        rows = db.execute_select("SELECT * FROM {}.{} WHERE user_name = '{}' AND chat_id='{}'"
                                 .format(
                                     self.config.get("postgresql","schema"),
                                     self.config.get("postgresql","users_table"),
                                     first_name,
                                     chat_id))
        if len(rows)!=0 and rows[0][4]==1:
            twillio_message = str(random.randint(0,9)) + str(random.randint(0,9)) + str(random.randint(0,9)) + str(random.randint(0,9))
            if (number[0]=="7"):
                number="+" + number
            db.execute_sql("UPDATE {}.{} SET phone_number='{}', sms_code='{}', status={}  WHERE user_name='{}' and chat_id='{}'"
                           .format(
                               self.config.get("postgresql","schema"),
                               self.config.get("postgresql","users_table"),
                               number,
                               twillio_message,
                               2,
                               first_name,
                               chat_id))
            LogDBHandler.log(Actions.phone_number_recieved, self.config.get("botsettings","messenger_name"), number, "", "success", "user", chat_id)
            Twilio.send_sms(twillio_message, number)
            LogDBHandler.log(Actions.code_sended,self.config.get("botsettings","messenger_name"),number,"","success","bot",chat_id)
            self.send_message(self.config.get("messages","confirmation_message"), chat_id)

    #обработка кода смс сообщения
    def try_process_code(self,  first_name, chat_id, number):
        db = DbHelper()
        rows = db.execute_select("SELECT * FROM {}.{} WHERE user_name = '{}' AND chat_id='{}'"
                                 .format(
                                     self.config.get("postgresql","schema"),
                                     self.config.get("postgresql","users_table"),
                                     first_name,
                                     chat_id))
        if len(rows)!=0 and rows[0][4]==2:
            if int(number)==rows[0][5]:
                db.execute_sql("UPDATE {}.{} SET status={}  WHERE user_name='{}' and chat_id='{}'"
                               .format(
                                   self.config.get("postgresql","schema"),
                                   self.config.get("postgresql","users_table"),
                                   3,
                                   first_name,
                                   chat_id))
                self.send_message(self.config.get("messages","subscribed_message"), chat_id)
                LogDBHandler.log(Actions.registred,self.config.get("botsettings","messenger_name"),number,"","success","bot",chat_id)
            else:
                LogDBHandler.log(Actions.registred,self.config.get("botsettings","messenger_name"),number,"","failed","bot",chat_id,"wrong confirmation code")
                self.send_message(self.config.get("messages","wrong_code_message"), chat_id)
                

    #получить роль пользователя
    def get_user_role(self, first_name, chat_id):
        db = DbHelper()
        rows = db.execute_select("SELECT role FROM {}.{} WHERE user_name = '{}' AND chat_id='{}'"
                                 .format(
                                     self.config.get("postgresql","schema"),
                                     self.config.get("postgresql","users_table"),
                                     first_name, 
                                     chat_id))
        if len(rows)!=0:
            return str(rows[0][0])
        else:
            return None
     
    #функция сканирует наличие сообщений в подключенных таблицах и отправляет если есть что оправить       
    def send_notifications(self):
        if self.started:

            for table in self.tables:
                for sheet in table.sheets:
                    
                    amount_re = re.compile(r'Надо(\n|\s|\t)+отправить.*')
                    #print(sheet.wks.get_all_records())                    
                    list_of_cells = sheet.wks.findall(amount_re)
                    
                    for cell in list_of_cells:
                        row = str(cell.row)
                        number = sheet.wks.acell(sheet.number + row).value.replace('-', '').replace('(', '').replace(')', '')
                        if (number[0]=="7"):
                            number="+" + number
                        elif (number[0]==8):
                            number = number.replace("8","+7",1)
                        db = DbHelper()
                        chat_id = db.execute_select("SELECT chat_id FROM {}.{} where phone_number='{}'"
                                                    .format(
                                                        self.config.get("postgresql","schema"),
                                                        self.config.get("postgresql","users_table"),
                                                        number))[0][0]
                        if ("sms" in str(cell.value)):
                            Twilio.send_sms(sheet.wks.acell(sheet.text + row).value,number)
                            sheet.wks.update_acell(sheet.sended+row,"Да")
                            LogDBHandler.log(Actions.finish_sending,"sms",number)
                        else:
                            if self.send_message(sheet.wks.acell(sheet.text + row).value,chat_id):
                                sheet.wks.update_acell(sheet.sended+row,"Да")
                                sheet.wks.update_acell(sheet.status+row,"отправлено")
                        time.sleep(float(self.config.get("botsettings","sending_interval")))
                           
    #вспомогательная функция для кастомной клавиатуры                        
    def build_keyboard(self, text):
        keyboard = [[{"text": text,"request_contact": True}]]
        reply_markup = {"keyboard":keyboard, "one_time_keyboard": True, "resize_keyboard": True}
        return json.dumps(reply_markup)

    #(подписать пользователя (добавить в БД)
    def subscribe(self, first_name, chat_id):
        db = DbHelper()
        rows = db.execute_select("SELECT * FROM {}.{} WHERE user_name = '{}' AND chat_id = '{}'"
                                 .format(
                                     self.config.get("postgresql","schema"),
                                     self.config.get("postgresql","users_table"),
                                     first_name,
                                     chat_id))
        if len(rows)==0:
            db.execute_sql("INSERT INTO {}.{} (user_name, chat_id, status) VALUES ('{}', '{}', {})"
                           .format(
                               self.config.get("postgresql","schema"),
                               self.config.get("postgresql","users_table"),
                               first_name,
                               chat_id,
                               1))
            #keyboard = self.build_keyboard(telegram.KeyboardButton(text="Поделиться номером телефона", request_contact=True))
            LogDBHandler.log(Actions.registred, self.config.get("botsettings","messenger_name"), self.get_phone_number(chat_id), "", "success", "user", chat_id)
            self.send_message(self.config.get("messages","share_number_message"), chat_id, self.build_keyboard(self.config.get("messages","button_caption")))
        else:
            self.send_message(self.config.get("messages","already_subscribed_message"), chat_id)
    
    #отписать пользователя (удалить из БД)
    def unsubscribe(self, first_name, chat_id):
        db = DbHelper()
        db.execute_sql("DELETE FROM {}.{} WHERE user_name = '{}' AND phone_number = '{}'"
                       .format(
                           self.config.get("postgresql","schema"),
                           self.config.get("postgresql","users_table"),
                           first_name,
                           chat_id))
        LogDBHandler.log(Actions.unsubscribed, self.config.get("botsettings","messenger_name"), self.get_phone_number(chat_id), "", "success", "user", chat_id)
        self.send_message(self.config.get("messages","unsubscribed_message"), chat_id)

    #отправка сообщения
    def send_message(self, text, chat_id, reply_markup=None):
        LogDBHandler.log(Actions.start_sending,"telegram",self.get_phone_number(chat_id),text[0:255])
        response = '{}'
        try:
            text = urllib.parse.quote_plus(text)
            url = self.URL + "sendMessage?text={}&chat_id={}&parse_mode=Markdown".format(text, chat_id)
            if reply_markup:
                url += "&reply_markup={}".format(reply_markup)
            print(url)
            response = self.get_url(url)
            LogDBHandler.log(Actions.message, self.config.get("botsettings","messenger_name"), self.get_phone_number(chat_id), "", "success", "user", chat_id)
            return True
        except  Exception as e:
            LogDBHandler.log(Actions.message, self.config.get("botsettings","messenger_name"), self.get_phone_number(chat_id), "", "failed", "user", chat_id, str(e.message).replace("/","//").replace("'","\""), '{}', response)
            return False

    def get_phone_number(self, chat_id):
        db = DbHelper()
        rows = db.execute_select("SELECT phone_number FROM {}.{} WHERE chat_id='{}'"
                                 .format(
                                     self.config.get("postgresql","schema"),
                                     self.config.get("postgresql","users_table"),
                                     chat_id))
        if len(rows)!=0:
            return str(rows[0][0])
        else:
            return ""
Пример #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
def test_config():
    c = ConfigParser()
    c.set("logins.username", "test1")
    assert c.get("logins.username") == "test1"
    c.set("concurrency", 10)
    assert c.get("concurrency") == 10