__author__ = "Wendell Jones" import os import sqlalchemy from sqlalchemy import text from sqlalchemy import MetaData from sqlalchemy.ext.automap import automap_base from sqlalchemy.orm import Session from db.Tables import Createtables, CreateTempTables, Tables as dbTables from db.mssql.Merger import Charactermerge as MSCharactermerge from db.mssql.Merger import Jsonmerge as MSJsonmerge from db.mssql.Merger import SQLGenerate as MSSQLGenerate from db.mssql.Merger import Episodemerge as MSEpisodemerge from db.pgsql.Merger import Charactermerge as PGCharactermerge from db.pgsql.Merger import Updatemerge as PGUpdatemerge from db.mssql.Merger import Jsonmerge as PGJsonmerge from db.pgsql.Merger import SQLGenerate as PGSQLGenerate from db.pgsql.Merger import Episodemerge as PGEpisodemerge import api.tvmaze.episodes import api.tvmaze.cast import api.tvmaze.series from pymongo import MongoClient, ReplaceOne, InsertOne from tqdm import tqdm from datetime import datetime Createtables = Createtables CreateTempTables = CreateTempTables dbTables = dbTables class settype(object): def __init__(self, dbtype, apitype, options): self.dbclass = {} self.dbengine = {} self.apiengine = {} self.load(dbtype) self.dbcreate(dbtype, options) self.setapi(apitype) self.get_data() def load(self, dbtype): dbtype = dbtype.lower() if dbtype in ["pgsql", "mysql"]: Updatemerge = PGUpdatemerge Jsonmerge = PGJsonmerge Episodemerge = PGEpisodemerge Charactermerge = PGCharactermerge merger = PGSQLGenerate elif dbtype == "mssql": Jsonmerge = MSJsonmerge Charactermerge = MSCharactermerge merger = MSSQLGenerate Episodemerge = MSEpisodemerge self.dbclass["jmerge"] = Jsonmerge() self.dbclass["cmerge"] = Charactermerge() self.dbclass["Createtables"] = Createtables self.dbclass["CreateTemptables"] = CreateTempTables self.dbclass["Tables"] = Createtables self.dbclass["merger"] = merger() if dbtype == "pgsql": self.dbclass["Updatemerge"] = Updatemerge self.dbclass["Episodemerge"] = Episodemerge elif dbtype == "mssql": self.dbclass["Charactermerge"] = Charactermerge def dbcreate(self, dbtype, options): updatesBase = automap_base() dboBase = automap_base() connection_string = None if dbtype.lower() == "mssql": connection_string = ( "mssql+pymssql://" + options["mssqlusername"] + ":" + options["mssqlpassword"] + "@" + options["hostname"] + ":" + options["mssqlport"] + "/" + options["dbname"] ) elif dbtype.lower() == "pgsql": connection_string = ( "postgresql+psycopg2://" + options["pgsqlusername"] + ":" + options["pgsqlpassword"] + "@" + options["hostname"] + ":" + options["pgsqlport"] + "/" + options["dbname"] ) elif dbtype.lower() == "mysql": connection_string = ( "mysql+pymysql://" + options["mysqlusername"] + ":" + options["mysqlpassword"] + "@" + options["hostname"] + ":" + options["mysqlport"] + "/" + options["dbname"] ) engine = sqlalchemy.create_engine( connection_string, pool_size=20, max_overflow=20, pool_recycle=15 ) currentDir = os.path.dirname(os.path.abspath(__file__)) currentDir = os.path.dirname(currentDir) ddl_file_path = f"{currentDir}/db/ddl/recreate_updates_schema.sql" with open(ddl_file_path, "r") as file: sql = file.read() file with engine.connect() as conn: with conn as cursor: cursor.execute(text(sql)) updatesBase.prepare(engine, reflect=True, schema="updates") dboBase.prepare(engine, reflect=True, schema="dbo") metadata = sqlalchemy.MetaData("updates") metadata.reflect(bind=engine) dboMetadata = sqlalchemy.MetaData("dbo") dboMetadata.reflect(bind=engine) session = Session(engine) self.dbengine["engine"] = engine self.dbengine["metadata"] = metadata self.dbengine["dbometadata"] = dboMetadata self.dbengine["updatesBase"] = updatesBase self.dbengine["dboBase"] = dboBase self.dbengine["session"] = session return self.dbengine def setapi(self, apitype): if apitype.lower() == "tvmaze": self.apiengine["sprocess"] = api.tvmaze.series self.apiengine["cprocess"] = api.tvmaze.cast self.apiengine["eprocess"] = api.tvmaze.episodes return self.apiengine elif apitype.lower() == "thetvdb": pass def reflect_tables(self, engine, Metadata): meta = MetaData() meta.reflect(bind=engine) return meta.tables def get_data(self): return self.dbclass, self.dbengine, self.apiengine def __del__(self): pass class dbmongo(object): global mgclient global mgseries global mgepisodes global mgactors global mgcharacters global mgcrew def bulk_upsert_by_id(self, collection, docs, batch_size=1000): ops = [] for doc in docs: if "id" not in doc: # skip documents without an `id` field continue # replace existing document with same `id`, or insert if it does not exist ops.append(ReplaceOne({"id": doc["id"]}, doc, upsert=True)) if len(ops) >= batch_size: collection.bulk_write(ops, ordered=False) ops = [] if ops: collection.bulk_write(ops, ordered=False) def convert_updated_time(self, timestamp): new_timestamp = datetime.fromtimestamp(timestamp) update_time = str(new_timestamp).split(".")[0] return update_time def __init__(self, options): host = options["mghostname"] port = options["mgport"] print(f"Connecting to mongodb at {host}:{port}") try: self.connection_string = ( f"mongodb://{options['mghostname']}:{options['mgport']}" ) self.mgclient = MongoClient(self.connection_string) self.mgdb = self.mgclient[options["mgdbname"]] except BaseException as e: print(f"Unable to connect to mongodb.{e}\nExitting!") exit() else: self.mgseries = self.mgdb["series"] self.mgseries.create_index("id", unique=True) self.mgepisodes = self.mgdb["episodes"] self.mgepisodes.create_index("id", unique=True) self.mgactors = self.mgdb["actors"] self.mgactors.create_index("id", unique=True) self.mgcharacters = self.mgdb["characters"] self.mgcharacters.create_index("id", unique=True) self.mgcrew = self.mgdb["crew"] self.mgcrew.create_index("id", unique=True) print( f"Connection to mongodb at {options['mghostname']}:" f"{options['mgport']} successful." ) def load_json_file(self, cachefile, jget, lprint, seriesid, datatype): try: with open(cachefile, "r") as file: jsondata = file.read() datastring = jget.load(jsondata) data = jget.jconvert(datastring) return data except Exception: lprint.logprint( "info", f"Unable to open {datatype} file for {seriesid}" ) def process_series_data( self, seriesdata, jget, dbExec, dbengine, dboBase, lprint, countrydictionary, seriesid, ): try: seriesresults, networkchannel, country, webchannel = ( jget.processSeriesJson(seriesdata) ) except Exception: lprint.logprint( "info", f"Unable to process series file for {seriesid}" ) else: update_time = self.convert_updated_time(seriesdata["updated"]) seriesdata["updated"] = update_time self.update_country_data( country, countrydictionary, dbExec, dbengine, dboBase, dbengine["dbometadata"], lprint, ) self.update_network_data( networkchannel, countrydictionary, seriesresults, dbExec, dbengine, dboBase, lprint, ) self.update_webchannel_data( webchannel, countrydictionary, seriesresults, dbExec, dbengine, dboBase, lprint, ) #self.bulk_upsert_by_id(self.mgseries, seriesdata) self.insert_or_update_mongo( self.mgseries, seriesdata, "id" ) def process_cast_data(self, persondata, characterdata, seriesid, lprint): self.bulk_upsert_by_id(self.mgactors, persondata) self.bulk_upsert_by_id(self.mgcharacters, characterdata) def process_crew_data(self, crewdata, seriesid, jget, lprint): self.bulk_upsert_by_id(self.mgcrew, crewdata) def update_country_data( self, country, countrydictionary, dbExec, dbengine, dbBase, metadata, lprint ): if len(country) > 0: try: selectquery = ( f"select row_id, country_name from dbo.countrydata where " f"country_name = '{country['name'].replace("'", "''")}'" ) countrylisting = dbExec.rawsql_select( dbengine["engine"], selectquery, lprint ) for countryname in countrylisting: countrydictionary[countryname[1]] = countryname[0] except Exception as e: lprint.logprint( "info", ( ( f'Unable to process country data for ' f'{country["name"]}. {e}' ) ) ) else: if len(countrylisting) > 0: pass else: insert_sql = ( ( f"insert into dbo.countrydata " "(country_name, country_code, country_timezone) " f"values ('{country['name'].replace("'", "''")}', " f"'{country['code'].replace("'", "''")}', " f"'{country['timezone'].replace("'", "''")}')" ) ) dbExec.rawsql_insert( dbengine["engine"], insert_sql, lprint ) def update_network_data( self, networkchannel, countrydictionary, seriesresults, dbExec, dbengine, dboBase, lprint, ): if len(networkchannel) > 0: try: networkchannel["countryid"] = countrydictionary[ networkchannel["country"]["name"] ] seriesresults["networkid"] = networkchannel["id"] except Exception: pass def update_webchannel_data( self, webchannel, countrydictionary, seriesresults, dbExec, dbengine, dboBase, lprint, ): if len(webchannel) > 0: try: webchannel["countryid"] = countrydictionary[ webchannel["country"]["name"] ] seriesresults["webchannelid"] = webchannel["id"] except Exception: pass seriesresults["seriesid"] = seriesresults["id"] del seriesresults["webchannel"] def insert_or_update_mongo(self, collection, data, unique_key): try: collection.insert_one(data) except Exception: filter_query = {unique_key: data[unique_key]} new_values = {"$set": data} del new_values["$set"]["_id"] collection.update_one(filter_query, new_values) def update_mongo( self, updateList, directories, jget, countrydictionary, dbExec, dbengine, dboBase, lprint, ): print("Now inserting data into tables.") series_failed_count = 0 episode_failed_count = 0 cast_failed_count = 0 crew_failed_count = 0 processed_count = 0 failed_file_count = 0 def process_data( file_path, data_type, failed_file_count, lprint ): """Helper function to load and process data.""" data = self.load_json_file( file_path, jget, lprint, seriesid, data_type ) if data: if "episode" in file_path: try: for item in data: item["seriesid"] = seriesid except Exception as e: lprint.logprint( "info", f"Unable to add {data_type} to data. {e}" ) if "cast" in file_path: persondata = [] characterdata = [] for item in data: update_time = self.convert_updated_time(item["person"]["updated"]) item["person"]["updated"] = update_time if "person" in item: item["person"]["seriesid"] = seriesid persondata.append(item["person"]) if "character" in item: item["character"]["seriesid"] = seriesid characterdata.append(item["character"]) try: self.bulk_upsert_by_id(self.mgactors, persondata) self.bulk_upsert_by_id(self.mgcharacters, characterdata) failed_file_count = "N/A" except Exception as e: failed_file_count = 1 lprint.logprint( "info", f"Unable to process {data_type} data. {e}" ) if "crew" in file_path: try: crewdata = [] except Exception as e: lprint.logprint( "info", f"Unable to add {data_type} data. {e}" ) else: for item in data: item["person"]["seriesid"] = seriesid item["person"]["type"] = item["type"] update_time =self.convert_updated_time(item["person"]["updated"]) if update_time: item["person"]["updated"] = str(update_time).split(".")[0] if "person" in item: crewdata.append(item["person"]) try: self.bulk_upsert_by_id(self.mgcrew, crewdata) failed_file_count = "N/A" return failed_file_count except Exception as e: failed_file_count = 1 lprint.logprint( "info", f"Unable to process {data_type} data.{e}" ) return failed_file_count return data for series in tqdm( range(len(updateList)), desc="Update MongoDB", unit="series" ): seriesid = updateList[series] seriescachefile = f'{directories["SERIESDIR"]}{seriesid}.json' episodecachefile = f'{directories["EPISODEDIR"]}{seriesid}.json' castcachefile = f'{directories["CASTDIR"]}{seriesid}.json' crewcachefile = f'{directories["CREWDIR"]}{seriesid}.json' # Process series data seriesdata = self.load_json_file( seriescachefile, jget, lprint, seriesid, "series" ) if seriesdata: try: self.process_series_data( seriesdata, jget, dbExec, dbengine, dboBase, lprint, countrydictionary, seriesid, ) self.bulk_upsert_by_id(self.mgseries, seriesdata) except Exception as e: series_failed_count += 1 lprint.logprint( "info", ( f"Unable to process series data for seriesid " f"{seriesid}. {e}" ), ) else: processed_count += 1 fcount = process_data( episodecachefile, "episode", failed_file_count, lprint, ) if isinstance(fcount, int): episode_failed_count = episode_failed_count + fcount else: try: self.bulk_upsert_by_id(self.mgepisodes, fcount) # for episode in fcount: # self.insert_or_update_mongo( # self.mgepisodes, episode, "id" # ) except Exception as e: lprint.logprint( "info", ( f"Unable to insert episode data for seriesid " f"{seriesid}. {e}" ), ) fcount = process_data( castcachefile, "cast",failed_file_count, lprint ) if isinstance(fcount, int): cast_failed_count = cast_failed_count + fcount if os.path.isfile(crewcachefile): fcount = process_data( crewcachefile, "crew", failed_file_count, lprint ) if isinstance(fcount, int): crew_failed_count = crew_failed_count + fcount else: try: for crew in fcount: self.insert_or_update_mongo( self.mgcrew, crew, "id" ) except Exception as e: lprint.logprint( "info", ( f"Unable to insert crew data for seriesid " f"{seriesid}. {e}" ), ) print(f"Processing complete. {processed_count} series processed.") print(f"Failed to load {series_failed_count} series files.") print(f"Failed to load {episode_failed_count} episode files.") print(f"Failed to load {crew_failed_count} crew files.") print(f"Failed to load {cast_failed_count} cast files.") def __del__(self): pass