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 ""
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
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