5 Commits

Author SHA1 Message Date
wjones 0fa8b83f2d add logging functionality to json downloading file. 2026-02-11 08:20:23 -05:00
wjones afe2dfa1c8 Update: Implement bulk update functionality using list of document IDs and updated data
This commit introduces a new function, `insert_mongo_documents_to_postgres`, which allows for the bulk updating of documents in a PostgreSQL table. It takes a list of document IDs and a dictionary containing the updated data as input. This method simplifies the process by reducing the number of individual insert statements required.

The changes include:

1. Adding a new function `insert_mongo_documents_to_postgres` to handle the bulk update.
2. Updating the existing code to call this new function when required.
3. Refactoring the `MongoDocumentInserter` class to encapsulate common functionality for document insertion and updating.

This refactoring enhances maintainability and efficiency by reducing redundancy in the codebase.
2026-02-10 12:01:22 -05:00
wjones c6f9d1e617 Update .gitignore and settings configuration: add archived directory and settings template 2026-02-07 12:29:55 -05:00
wjones 179fb4fddb Refactor code structure for improved readability and maintainability 2026-02-07 11:57:07 -05:00
wjones 8ed2a77365 Add MongoDB query utilities and update batch size for document insertion 2026-02-06 14:49:56 -05:00
12 changed files with 172594 additions and 121 deletions
+5 -1
View File
@@ -19,5 +19,9 @@ dbsync.log.*
.idea
/log
/venv
/temp/post_tvmaze_updates.json
/temp/post_tvmaze_updates.json.*
/archived
/temp/tvmaze_updates.json
/temp.skip_ids.txt
/archived
settings/tvsync_settings.cfg
+270
View File
@@ -0,0 +1,270 @@
"""MongoDB query utilities for filtering documents by update timestamp."""
__author__ = 'Wendell Jones'
from typing import Any, Dict, List, Optional
from datetime import datetime, timedelta, timezone
import logging
logger = logging.getLogger(__name__)
class MongoQueryUtils:
"""Utilities for querying MongoDB collections."""
@staticmethod
def get_recent_updates(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> List[Dict[str, Any]]:
"""
Find documents updated in the last N minutes.
Handles both epoch timestamps (int/float) and ISO datetime strings.
Args:
collection: PyMongo collection object.
minutes: Number of minutes to look back (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
query_filter: Additional MongoDB query filter to apply (optional).
Returns:
List of documents updated within the time window.
Example:
from db.query_utils import MongoQueryUtils
recent = MongoQueryUtils.get_recent_updates(
collection=mongo_db['series'],
minutes=60,
updated_field='updated',
)
print(f"Found {len(recent)} recently updated documents")
"""
if query_filter is None:
query_filter = {}
# Calculate cutoff time (60 minutes ago)
now = datetime.now(timezone.utc)
cutoff_time = now - timedelta(minutes=minutes)
cutoff_epoch = int(cutoff_time.timestamp())
logger.info(
f"Searching for documents updated in last {minutes} minutes "
f"(since {cutoff_time.isoformat()})"
)
# Build query for documents updated after cutoff
# Support both epoch timestamps and ISO datetime strings
query = {
**query_filter,
'$or': [
# Epoch timestamp (int/float)
{updated_field: {'$gte': cutoff_epoch}},
# ISO datetime string
{updated_field: {'$gte': cutoff_time.isoformat()}},
]
}
documents = list(collection.find(query))
logger.info(f"Found {len(documents)} documents matching criteria")
return documents
@staticmethod
def get_recent_updates_by_id(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
id_field: str = 'id',
query_filter: Optional[Dict] = None,
) -> List[int]:
"""
Get IDs of documents updated in the last N minutes.
Useful for batch processing or sync operations.
Args:
collection: PyMongo collection object.
minutes: Number of minutes to look back (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
id_field: Name of the ID field (default: 'id').
query_filter: Additional MongoDB query filter (optional).
Returns:
List of document IDs that were recently updated.
Example:
ids = MongoQueryUtils.get_recent_updates_by_id(
collection=mongo_db['series'],
minutes=60,
)
print(f"Series IDs to update: {ids}")
"""
docs = MongoQueryUtils.get_recent_updates(
collection=collection,
minutes=minutes,
updated_field=updated_field,
query_filter=query_filter,
)
ids = [doc.get(id_field) for doc in docs if id_field in doc]
return ids
@staticmethod
def get_updates_since(
collection: Any,
since_timestamp: Optional[Any] = None,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> List[Dict[str, Any]]:
"""
Find documents updated since a specific timestamp or N minutes ago.
Args:
collection: PyMongo collection object.
since_timestamp: Epoch int, float, or ISO datetime string (optional).
If None, defaults to N minutes ago (see minutes parameter).
minutes: If since_timestamp is None, look back this many minutes (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
query_filter: Additional MongoDB query filter (optional).
Returns:
List of documents updated since the timestamp.
Example:
# Get updates since a specific timestamp
cutoff = 1704067200 # Epoch timestamp
docs = MongoQueryUtils.get_updates_since(
collection=mongo_db['series'],
since_timestamp=cutoff,
)
# Or use default 60 minutes ago
docs = MongoQueryUtils.get_updates_since(
collection=mongo_db['series'],
)
# Or specify different time window
docs = MongoQueryUtils.get_updates_since(
collection=mongo_db['series'],
minutes=120, # Last 2 hours
)
"""
if query_filter is None:
query_filter = {}
# If no timestamp provided, use minutes parameter to calculate cutoff
if since_timestamp is None:
now = datetime.now(timezone.utc)
cutoff_time = now - timedelta(minutes=minutes)
since_timestamp = int(cutoff_time.timestamp())
logger.info(f"Using default {minutes} minutes ago: {cutoff_time.isoformat()}")
query = {
**query_filter,
'$or': [
{updated_field: {'$gte': since_timestamp}},
{updated_field: {'$gte': str(since_timestamp)}},
]
}
documents = list(collection.find(query))
logger.info(f"Found {len(documents)} documents updated since {since_timestamp}")
return documents
@staticmethod
def count_recent_updates(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> int:
"""
Count documents updated in the last N minutes.
Args:
collection: PyMongo collection object.
minutes: Number of minutes to look back (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
query_filter: Additional MongoDB query filter (optional).
Returns:
Count of recently updated documents.
Example:
count = MongoQueryUtils.count_recent_updates(
collection=mongo_db['series'],
minutes=60,
)
print(f"{count} series were updated in the last hour")
"""
if query_filter is None:
query_filter = {}
now = datetime.now(timezone.utc)
cutoff_time = now - timedelta(minutes=minutes)
cutoff_epoch = int(cutoff_time.timestamp())
query = {
**query_filter,
'$or': [
{updated_field: {'$gte': cutoff_epoch}},
{updated_field: {'$gte': cutoff_time.isoformat()}},
]
}
count = collection.count_documents(query)
return count
# Convenience functions
def get_recent_updates(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> List[Dict[str, Any]]:
"""Find documents updated in the last N minutes."""
return MongoQueryUtils.get_recent_updates(
collection=collection,
minutes=minutes,
updated_field=updated_field,
query_filter=query_filter,
)
def get_recent_ids(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
id_field: str = 'id',
query_filter: Optional[Dict] = None,
) -> List[int]:
"""Get IDs of documents updated in the last N minutes."""
return MongoQueryUtils.get_recent_updates_by_id(
collection=collection,
minutes=minutes,
updated_field=updated_field,
id_field=id_field,
query_filter=query_filter,
)
def count_recent(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> int:
"""Count documents updated in the last N minutes."""
return MongoQueryUtils.count_recent_updates(
collection=collection,
minutes=minutes,
updated_field=updated_field,
query_filter=query_filter,
)
+99 -10
View File
@@ -335,7 +335,7 @@ def create_postgres_table_from_mongo(
class MongoDocumentInserter:
"""Insert MongoDB documents into PostgreSQL tables with type conversion."""
def __init__(self, batch_size: int = 1000):
def __init__(self, batch_size: int = 10000):
"""
Initialize the document inserter.
@@ -530,17 +530,106 @@ class MongoDocumentInserter:
if on_conflict:
insert_sql += f" {on_conflict}"
with engine.begin() as conn:
if on_conflict:
# Use different strategies depending on whether ON CONFLICT is needed.
# For ON CONFLICT updates, per-row execution is slow; use psycopg2's
# execute_values to perform fast multi-row INSERT ... VALUES (...) ON CONFLICT ...
if on_conflict:
try:
# Prepare ordered tuples for insertion
values_list = []
for doc in prepared:
values_list.append(tuple(doc.get(col) for col in column_names))
# Build base INSERT with placeholder for execute_values
insert_base = f"INSERT INTO {full_table_name} ({cols_str}) VALUES %s {on_conflict}"
# Use raw DB-API connection for execute_values
try:
import psycopg2
except Exception:
# Fall back to SQLAlchemy executemany if psycopg2 not available
with engine.begin() as conn:
for doc in prepared:
try:
conn.execute(text(insert_sql), [doc])
total_inserted += 1
except Exception as e:
logger.error(f"Error inserting document into {full_table_name}: {e}")
raise
logger.info(f"Inserted {total_inserted} documents into {full_table_name}")
else:
# Use a single staging temporary table per insert_documents call.
# Stream each batch into the temp table via COPY, then run one
# INSERT ... SELECT ... ON CONFLICT ... to upsert all rows.
raw_conn = engine.raw_connection()
try:
conn.execute(text(insert_sql), [doc])
total_inserted += 1
except Exception as e:
logger.error(f"Error inserting document into {full_table_name}: {e}")
raise
logger.info(f"Inserted {total_inserted} documents into {full_table_name}")
else:
cur = raw_conn.cursor()
import io
import csv
import time
import os
temp_name = f"tmp_{table_name}_{int(time.time())}_{os.getpid()}"
try:
# Create temporary table once
cur.execute(f"CREATE TEMP TABLE {temp_name} (LIKE {full_table_name} INCLUDING ALL);")
# Stream batches into temp table
for i in range(0, len(prepared), self.batch_size):
batch = prepared[i:i + self.batch_size]
try:
sio = io.StringIO()
writer = csv.writer(sio)
for doc in batch:
row = []
for col in column_names:
val = doc.get(col)
if val is None:
row.append('\\N')
else:
row.append(val)
writer.writerow(row)
sio.seek(0)
copy_sql = f"COPY {temp_name} ({cols_str}) FROM STDIN WITH CSV NULL '\\N'"
cur.copy_expert(copy_sql, sio)
raw_conn.commit()
batch_count = len(batch)
total_inserted += batch_count
logger.info(f"Copied {batch_count} rows into staging {temp_name} (total staged: {total_inserted})")
except Exception as e:
raw_conn.rollback()
logger.error(f"Error copying batch into {temp_name}: {e}")
raise
# Perform a single upsert from the staging table
try:
insert_from_temp = f"INSERT INTO {full_table_name} ({cols_str}) SELECT {cols_str} FROM {temp_name} {on_conflict}"
cur.execute(insert_from_temp)
raw_conn.commit()
logger.info(f"Upserted staged rows from {temp_name} into {full_table_name}")
except Exception as e:
raw_conn.rollback()
logger.error(f"Error upserting from {temp_name} into {full_table_name}: {e}")
raise
finally:
# Temp table will be dropped on commit/connection close, ensure commit
try:
raw_conn.commit()
except Exception:
pass
finally:
try:
cur.close()
except Exception:
pass
try:
raw_conn.close()
except Exception:
pass
except Exception:
raise
else:
with engine.begin() as conn:
for i in range(0, len(prepared), self.batch_size):
batch = prepared[i:i + self.batch_size]
try:
+48 -2
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
#!/usr/bin/python3
import os
import json
import requests
import time
import logging
def download_json_to_file(api_url, outputdir, output_file, lprint):
"""
@@ -15,8 +16,53 @@ def download_json_to_file(api_url, outputdir, output_file, lprint):
data = response.json()
with open(f"{outputdir}{output_file}", 'w') as f:
json.dump(data, f, indent=4)
#print(f"JSON data saved to {outputdir}{output_file}")
# print(f"JSON data saved to {outputdir}{output_file}")
time.sleep(0.5) # Sleep for 0.5 second to avoid overwhelming the server
except Exception as e:
lprint.logprint("info", f"Downloadinng {outputdir}{output_file} for series ID: {output_file.replace('.json', '')} failed: {e}")
def main():
# Download data from an API endpoint to a specified file in JSON format
api_url = 'https://sample-api.com/data'
outputdir = '/home/user/'
outputfile = 'data.json'
lprint = Logprint('output')
try:
download_json_to_file(api_url, outputdir, outputfile)
print("Download successful!")
except Exception as e:
# Handle any exceptions that occur during the download process and log them
lprint.logprint("info", f"Downloading {outputdir}{outputfile} failed with error:{e}")
exit(1)
logger = logging.getLogger(__name__) # Use __name__ as the name for the logger
formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(message)s')
fh = logging.FileHandler('logfilename.txt') # Create a file handler object
fh.setFormatter(formatter)
sh = logging.StreamHandler() # Create a stream handler object (for console output)
sh.setFormatter(formatter)
logger.setLevel(logging.INFO) # Set the level of messages to log. Possible values are DEBUG, INFO, WARNING, ERROR, and CRITICAL. Default is INFO
if not logger.hasHandlers():
logger.addHandler(fh) # Add file handler object to the logger object
if not logger.handlers:
logger.addHandler(sh) # Add stream handler object to the logger object
try:
download_json_to_file() # Your function that you want to log the output of
except Exception as e: # Handle exceptions if any occur
logger.error(f"Error while running {download_json_to_file}: {e}")
else:
# If no exceptions are encountered, write a INFO message to the log file
logger.info(f"{download_json_to_file} completed successfully.")
if __name__ == "__main__":
main()
+2 -2
View File
@@ -44,12 +44,12 @@ class LPrint:
"""Check the log file size and rotate if necessary."""
log_size = os.path.getsize(self.logfile)
print(f'Logfile Size: {log_size} ({self.humanbytes(log_size)})')
if force_rotate or log_size > 10_000_000: # 10 MB
if force_rotate or log_size > 50_000_000: # 10 MB
self.rotate_logs()
def rotate_logs(self):
"""Rotate the log file."""
from logrotater import LogRotate # Assuming logrotater is a custom module
from logs.logrotater import LogRotate # Assuming logrotater is a custom module
rotater = LogRotate(prefix=self.logfile, verbose=True)
try:
rotater.rotate()
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python
''' A python logrotate library'''
import os
import glob
__author__ = 'Kyle Laplante'
__copyright__ = 'Copyright 2013, Kyle Laplante'
__date__ = '07-07-2013'
__license__ = 'GPL'
__version__ = '1.3'
__email__ = 'kyle.laplante@gmail.com'
class LogRotateException(Exception):
pass
class LogRotate:
"""This library is a simple logrotater (or file rotater).
Simply pass in the path to the main logfile and this
library will rotate all the logs by an increment of 1.
Example:
import logrotater
rotater = logrotater.LogRotate(prefix='/home/kyle/p4.log', verbose=True)
rotater.rotate()
The prefix path should be the path of the main logfile without the .N extension.
The previous example would rotate /home/kyle/p4.log.N to /home/kyle/p4.log.N+1,
move /home/kyle/p4.log to /home/kyle/p4.log.1 and create a new empty /home/kyle/p4.log."""
def __init__(self, *args, **kwargs):
'''Required kwargs: prefix="some path"
Optional kwargs: verbose=True
If verbose is True then LogRotate will print what it is doing with each file.'''
self.verbose = kwargs.get('verbose', False)
for k, v in kwargs.items():
setattr(self, k, v)
if not os.path.isfile(self.prefix):
raise LogRotateException("Must use a valid prefix filename")
def rotate(self, *args, **kwargs):
self.files = glob.glob(self.prefix + "*")
if not len(self.files) > 0:
raise LogRotateException(
"No files with the prefix %s found" % self.prefix)
self.nums = []
for file in self.files:
try:
self.nums.append(int(file.split(self.prefix + '.')[-1]))
except ValueError:
pass
self.nums.sort()
self.nums.reverse()
for num in self.nums:
new_num = num + 1
if self.verbose:
print("moving %s.%d to %s.%d" % (self.prefix, num, self.prefix, new_num))
os.rename("%s.%d" % (self.prefix, num), "%s.%d" %
(self.prefix, new_num))
if self.verbose:
print("moving %s to %s.1" % (self.prefix, self.prefix))
os.rename(self.prefix, self.prefix + '.1')
self.touch(self.prefix)
def touch(self, path):
'''a simple method to touch a file'''
if self.verbose:
print("Creating file: %s" % path)
f = open(path, 'a')
os.utime(path, None)
if __name__ == '__main__':
from optparse import OptionParser
import sys
usage = '''%prog [options] <file prefix>
File prefix is the path to the main logfile.'''
parser = OptionParser(usage=usage)
parser.add_option(
'-p', '--prefix', dest='prefix', default=None, help='Filename prefix.')
parser.add_option('-v', '--verbose', dest='verbose',
action='store_true', default=False, help='Enable verbose output')
(opts, args) = parser.parse_args()
if opts.prefix is None:
print("Please enter a filename prefix")
sys.exit(1)
try:
rotater = LogRotate(prefix=opts.prefix, verbose=opts.verbose)
except LogRotateException as e:
print(e)
sys.exit(1)
try:
rotater.rotate()
except LogRotateException as e:
print(e)
sys.exit(1)
+379 -105
View File
@@ -1,120 +1,394 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import logging
import sys
from tqdm import tqdm
import logs.Logger
from typing import Dict, Tuple, Any
from db.functions import settype, dbmongo
from datetime import datetime
import json
from db.schema_generator import MongoToPostgresSchemaGenerator, MongoDocumentInserter
import settings.config
def set_apienv(urls, uprocess, dbengine, dbExec, updatesBase, lprint):
"""Populate updates table from the API and return available updates.
Args:
urls: URL configuration with an "updatesurl" key.
uprocess: URL loader instance with `get_data`.
jget: JSON processor with `jconvert`.
dbengine: database engine dict (expects key "engine").
dbExec: database execution helper with `update_tvupdates` and `rawsql_select`.
updatesBase: list-like containing tables (uses index 8 for updates table).
lprint: logger instance with `logprint`.
Returns:
List of tuples (seriesid, timestamp) representing available updates.
"""
#updatetable = updatesBase[8]
#inputdata = uprocess.get_data(urls["updatesurl"])
#lprint.logprint("info", f"Retrieved {len(availableupdates)} rows for processing......")
#
newupdates = dbExec.rawsql_select(
dbengine["engine"],
"select seriesid,timestamp from updates.tvupdates",
lprint
)
return newupdates
ROOTDIR = os.getcwd()
config_options = settings.config.Config(ROOTDIR)
options = config_options.config_options
dbtype, apitype = options["dbtype"], options["apitype"]
dbs = settype(dbtype, apitype, options)
dbclass, dbengine = dbs.dbclass, dbs.dbengine
mongo_updater = dbmongo(options)
generator = MongoToPostgresSchemaGenerator(sample_size=100)
# Configuration constants
SAMPLE_SIZE = 10000
BATCH_SIZE = 10000
SCHEMA_NAME = 'updates'
PK_FIELD = 'id'
tableNames = ['seriesdata', 'episodesdata', 'actorsdata', 'charactersdata', 'crewdata']
for tableName in tableNames:
# Generate and print the schema for each collection
schema = generator.analyze_collection(mongo_updater, getattr(mongo_updater, f'mg{tableName[:-4]}'))
sql = generator.generate_create_table_sql(
tableName,
schema_name='updates',
pk_field='id' if tableName == 'seriesdata' else 'id',
)
print(f"--- SQL Schema for {tableName} ---")
print(sql)
print("\n")
# Or create it directly
generator.create_table_in_postgres(
dbengine,
tableName,
schema_name='updates',
#pk_field='seriesid' if tableName == 'seriesdata' else 'id',
pk_field='id',
drop_existing=True,
)
inserter = MongoDocumentInserter(batch_size=1000)
# Map table names to their MongoDB collections and primary keys
collection_map = {
'seriesdata': (mongo_updater.mgseries, 'id'),
'episodesdata': (mongo_updater.mgepisodes, 'id'),
'actorsdata': (mongo_updater.mgactors, 'id'),
'charactersdata': (mongo_updater.mgcharacters, 'id'),
'crewdata': (mongo_updater.mgcrew, 'id'),
# Table-to-collection mapping
TABLE_CONFIG = {
'seriesdata': 'mgseries',
'episodesdata': 'mgepisodes',
'actorsdata': 'mgactors',
'charactersdata': 'mgcharacters',
'crewdata': 'mgcrew',
}
# Insert documents from collections with UPSERT
for tableName in tableNames:
if tableName in collection_map:
collection, pk_field = collection_map[tableName]
# Build ON CONFLICT ... DO UPDATE clause
# Re-analyze the specific collection so we have the correct fields
schema = generator.analyze_collection(mongo_updater, collection)
# Use lowercased column names to match PostgreSQL unquoted identifiers
update_cols = [col.lower() for col in schema.keys() if col.lower() != pk_field.lower() and col != '_id']
update_clause = ', '.join([f"{col}=EXCLUDED.{col}" for col in update_cols])
on_conflict = f"ON CONFLICT ({pk_field}) DO UPDATE SET {update_clause}"
def setup_logger() -> logging.Logger:
"""Initialize and return configured logger."""
logger = logging.getLogger(__name__)
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
def initialize_environment(root_dir: str) -> Tuple[Dict[str, Any], Any, Any]:
"""Initialize config, database engine, and MongoDB updater.
Args:
root_dir: Root directory path
count = inserter.insert_from_collection(
engine=dbengine,
table_name=tableName,
collection=collection,
schema_name='updates',
on_conflict=on_conflict,
)
print(f"Inserted/Updated {count} documents in {tableName}")
else:
print(f"Warning: No collection mapping for {tableName}")
Returns:
Tuple of (options dict, db engine, mongo updater)
"""
logger = logging.getLogger(__name__)
try:
config_options = settings.config.Config(root_dir)
options = config_options.config_options
if not options:
raise ValueError("Configuration is empty")
dbtype = options.get('dbtype')
apitype = options.get('apitype')
if not dbtype or not apitype:
raise ValueError("Missing required config options: dbtype, apitype")
dbs = settype(dbtype, apitype, options)
dbengine = dbs.dbengine
mongo_updater = dbmongo(options)
logger.info("Environment initialized successfully")
return options, dbengine, mongo_updater
except Exception as e:
logger.error(f"Failed to initialize environment: {e}")
raise
# # Or directly from MongoDB collection
# count = inserter.insert_from_collection(
# engine=dbengine,
# table_name='seriesdata',
# collection=mongo_db['series'],
# schema_name='dbo',
# on_conflict="DO NOTHING",
# )
# if __name__ == "__main__":
# sys.exit(main())
def create_tables(
generator: MongoToPostgresSchemaGenerator,
mongo_updater: Any,
dbengine: Dict[str, Any],
table_config: Dict[str, str],
logger: logging.Logger
) -> Dict[str, Dict[str, Any]]:
"""Create PostgreSQL tables and return cached schemas.
Args:
generator: Schema generator instance
mongo_updater: MongoDB updater instance
dbengine: Database engine configuration
table_config: Mapping of table names to MongoDB collection attributes
logger: Logger instance
Returns:
Dict mapping table names to their schemas
"""
schemas = {}
for table_name, collection_attr in table_config.items():
try:
# Get MongoDB collection
collection = getattr(mongo_updater, collection_attr, None)
if not collection:
logger.warning(f"Collection '{collection_attr}' not found on mongo_updater")
continue
# Analyze schema
schema = generator.analyze_collection(mongo_updater, collection)
schemas[table_name] = schema
# Generate and display SQL
sql = generator.generate_create_table_sql(
table_name,
schema_name=SCHEMA_NAME,
pk_field=PK_FIELD,
)
logger.info(f"Schema for {table_name}:\n{sql}")
# Create table in PostgreSQL
generator.create_table_in_postgres(
dbengine,
table_name,
schema_name=SCHEMA_NAME,
pk_field=PK_FIELD,
drop_existing=True,
)
logger.info(f"Table '{table_name}' created/recreated successfully")
except Exception as e:
logger.error(f"Failed to create table '{table_name}': {e}")
continue
return schemas
def build_upsert_clause(schema: Dict[str, Any], pk_field: str) -> str:
"""Build SQL ON CONFLICT clause for upsert.
Args:
schema: Column schema dictionary
pk_field: Primary key field name
Returns:
ON CONFLICT clause string
"""
# Exclude primary key and MongoDB's _id field from updates
update_cols = [
col.lower() for col in schema.keys()
if col.lower() != pk_field.lower() and col != '_id'
]
if not update_cols:
return f"ON CONFLICT ({pk_field}) DO NOTHING"
update_clause = ', '.join([f"{col}=EXCLUDED.{col}" for col in update_cols])
return f"ON CONFLICT ({pk_field}) DO UPDATE SET {update_clause}"
def should_update_document(
doc_id: Any,
doc_updated: Any,
dbengine: Dict[str, Any],
table_name: str,
schema_name: str,
logger: logging.Logger
) -> bool:
"""Compare document's updated timestamp to the database row's updated timestamp.
Args:
doc_id: Document ID from MongoDB
doc_updated: Updated timestamp from MongoDB document
dbengine: Database engine configuration
table_name: Target table name
schema_name: Target schema name
logger: Logger instance
Returns:
True if document should be updated (MongoDB is newer), False otherwise
"""
if doc_updated is None:
logger.debug(f"Document {doc_id} has no 'updated' field, will update")
return True
try:
from sqlalchemy import text
# Query the database for the existing record's updated timestamp
query = text(f"""
SELECT updated FROM {schema_name}.{table_name}
WHERE id = :id
""")
with dbengine['engine'].connect() as conn:
result = conn.execute(query, {'id': doc_id}).fetchone()
# If record doesn't exist, update it
if result is None:
logger.debug(f"Record {doc_id} not in DB, will insert")
return True
db_updated = result[0]
# Compare timestamps
if db_updated is None:
logger.debug(f"Record {doc_id} has NULL updated in DB, will update")
return True
# Only update if MongoDB version is newer
should_update = doc_updated > db_updated
if not should_update:
logger.debug(f"Record {doc_id}: DB updated={db_updated} >= doc updated={doc_updated}, skipping")
else:
logger.debug(f"Record {doc_id}: doc updated={doc_updated} > DB updated={db_updated}, updating")
return should_update
except Exception as e:
logger.warning(f"Error comparing timestamps for {doc_id}: {e}. Proceeding with update.")
return True
def insert_documents(
inserter: MongoDocumentInserter,
mongo_updater: Any,
dbengine: Dict[str, Any],
table_config: Dict[str, str],
schemas: Dict[str, Dict[str, Any]],
logger: logging.Logger,
compare_timestamps: bool = True
) -> None:
"""Insert/upsert documents from MongoDB collections to PostgreSQL.
Args:
inserter: Document inserter instance
mongo_updater: MongoDB updater instance
dbengine: Database engine configuration
table_config: Mapping of table names to MongoDB collection attributes
schemas: Cached schemas from table creation
logger: Logger instance
compare_timestamps: If True, only update if MongoDB doc is newer than DB record
"""
total_inserted = 0
total_skipped = 0
for table_name, collection_attr in table_config.items():
if table_name not in schemas:
logger.warning(f"Skipping '{table_name}' - schema not available")
continue
try:
# Get collection and schema
collection = getattr(mongo_updater, collection_attr)
schema = schemas[table_name]
# Build upsert clause
on_conflict = build_upsert_clause(schema, PK_FIELD)
# If timestamp comparison is enabled, filter documents before inserting
if compare_timestamps:
logger.info(f"Filtering {table_name} documents by updated timestamp and batching every {BATCH_SIZE}...")
batch = []
batch_num = 0
for doc in collection.find():
doc_id = doc.get(PK_FIELD)
doc_updated = doc.get('updated')
if should_update_document(doc_id, doc_updated, dbengine, table_name, SCHEMA_NAME, logger):
batch.append(doc)
else:
total_skipped += 1
# When batch reaches size, insert and reset
if len(batch) >= BATCH_SIZE:
batch_num += 1
try:
# Explicit transaction per batch
with dbengine['engine'].begin():
count = inserter.insert_documents(
engine=dbengine,
table_name=table_name,
documents=batch,
schema_name=SCHEMA_NAME,
on_conflict=on_conflict,
)
total_inserted += count
logger.info(f"Batch {batch_num}: Inserted/Updated {count} documents into '{table_name}' (committed)")
except Exception as e:
logger.error(f"Batch {batch_num} failed for '{table_name}': {e}")
finally:
batch = []
# Insert any remaining docs in the final batch
if batch:
batch_num += 1
try:
with dbengine['engine'].begin():
count = inserter.insert_documents(
engine=dbengine,
table_name=table_name,
documents=batch,
schema_name=SCHEMA_NAME,
on_conflict=on_conflict,
)
total_inserted += count
logger.info(f"Final batch {batch_num}: Inserted/Updated {count} documents into '{table_name}' (committed)")
except Exception as e:
logger.error(f"Final batch {batch_num} failed for '{table_name}': {e}")
finally:
batch = []
else:
# Insert all documents without timestamp comparison
# Stream and batch inserts to avoid memory spikes
batch = []
batch_num = 0
for doc in collection.find():
batch.append(doc)
if len(batch) >= BATCH_SIZE:
batch_num += 1
try:
with dbengine['engine'].begin():
count = inserter.insert_documents(
engine=dbengine,
table_name=table_name,
documents=batch,
schema_name=SCHEMA_NAME,
on_conflict=on_conflict,
)
total_inserted += count
logger.info(f"Batch {batch_num}: Inserted/Updated {count} documents into '{table_name}' (committed)")
except Exception as e:
logger.error(f"Batch {batch_num} failed for '{table_name}': {e}")
finally:
batch = []
if batch:
batch_num += 1
try:
with dbengine['engine'].begin():
count = inserter.insert_documents(
engine=dbengine,
table_name=table_name,
documents=batch,
schema_name=SCHEMA_NAME,
on_conflict=on_conflict,
)
total_inserted += count
logger.info(f"Final batch {batch_num}: Inserted/Updated {count} documents into '{table_name}' (committed)")
except Exception as e:
logger.error(f"Final batch {batch_num} failed for '{table_name}': {e}")
finally:
batch = []
except Exception as e:
logger.error(f"Failed to insert documents into '{table_name}': {e}")
continue
logger.info(f"Total documents inserted/updated: {total_inserted}, skipped: {total_skipped}")
def main() -> int:
"""Main entry point for MongoDB to PostgreSQL sync."""
logger = setup_logger()
try:
root_dir = os.getcwd()
# Initialize environment
options, dbengine, mongo_updater = initialize_environment(root_dir)
# Create schema generator and inserter
generator = MongoToPostgresSchemaGenerator(sample_size=SAMPLE_SIZE)
inserter = MongoDocumentInserter(batch_size=BATCH_SIZE)
# Create tables and get cached schemas
logger.info("Creating PostgreSQL tables...")
schemas = create_tables(generator, mongo_updater, dbengine, TABLE_CONFIG, logger)
if not schemas:
logger.error("No schemas were successfully created. Aborting insert.")
return 1
# Insert documents from MongoDB
logger.info("Inserting documents from MongoDB...")
insert_documents(inserter, mongo_updater, dbengine, TABLE_CONFIG, schemas, logger)
logger.info("MongoDB to PostgreSQL sync completed successfully")
return 0
except Exception as e:
logger.error(f"Sync failed: {e}", exc_info=True)
return 1
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -5,7 +5,7 @@ loglevel=info
sqlserver_driver=ODBC Driver 13 for SQL Server
hostname=192.168.128.3
mghostname=192.168.128.24
dbname=media_dbsync2
dbname=media_dbsync
mgdbname=tvdata
mysqlusername=root
pgsqlusername=postgres
+86
View File
@@ -0,0 +1,86 @@
[global]
loglevel=info
[dbsettings]
sqlserver_driver=ODBC Driver 13 for SQL Server
hostname=192.168.128.3
mghostname=192.168.128.24
dbname=media_dbsync
mgdbname=tvdata
mysqlusername=root
pgsqlusername=postgres
mssqlusername=sa
mysqlpassword=Optimus0329
pgsqlpassword=Optimus0329
mssqlpassword=Optimus0329
pgsqlport=5432
mysqlport=3306
mssqlport=1433
mgport=27017
[api]
type=tvmaze
[json]
seriesapi=http://api.tvmaze.com/shows?page=<pagenum>
showapi=http://api.tvmaze.com/shows/<tvmazeid>
episodeapi=http://api.tvmaze.com/shows/<tvmazeid>/episodes
aliasapi=http://api.tvmaze.com/shows/<tvmazeid>/akas
castapi=http://api.tvmaze.com/shows/<tvmazeid>/cast
crewapi=http://api.tvmaze.com/shows/<tvmazeid>/crew
creditsapi=http://api.tvmaze.com/people/<actorid>/castcredits
updatesapi=http://api.tvmaze.com/updates/shows
[database]
dbtype=pgsql
updateschema=updates
initialize=0
initload=0
[updatetype]
#valid values are: daily, weekly, monthly, full, custom
type=full
full=full
monthly=1219200
weekly=604800
daily=86400
12h=23200
6h = 16600
# If settype = custom set below line to the number of seconds you want updates for.
custom=4150
cacheskip = 1
refreshcache = 0
# Field names for series insert query
# field01: nextepisode,
# field02: previousepisode,
# field03: mazeurl,
# field04: tvdbid,
# field05: tvrageid,
# field06: genres,
# field07: tvmazeid,
# field08: mediumimage,
# field09: originalimage,
# field10: showlanguage,
# field11: seriesname,
# field12: webchannelcountrycode,
# field13: webchannelcountryname,
# field14: webchannelcountrytimezone,
# field15: webchannelid,
# field16: webchannelname,
# field17: networkcountrycode,
# field18: networkcountryname,
# field19: networkcountrytimezone,
# field20: networkid,
# field21: networkname,
# field22: premierdate,
# field23: showrating,
# field24: showruntime,
# field25: airdays,
# field26: scheduletime,
# field27: showstatus,
# field28: showoverview,
# field29: showtype,
# field30: showupdated,
# field31: apilink,
# field32: showweight
File diff suppressed because it is too large Load Diff
+986
View File
@@ -0,0 +1,986 @@
137;http://api.tvmaze.com/people/137/castcredits
145;http://api.tvmaze.com/people/145/castcredits
137;http://api.tvmaze.com/people/137/castcredits
145;http://api.tvmaze.com/people/145/castcredits
211;http://api.tvmaze.com/people/211/castcredits
214;http://api.tvmaze.com/people/214/castcredits
315;http://api.tvmaze.com/people/315/castcredits
368;http://api.tvmaze.com/people/368/castcredits
416;http://api.tvmaze.com/people/416/castcredits
473;http://api.tvmaze.com/people/473/castcredits
557;http://api.tvmaze.com/people/557/castcredits
577;http://api.tvmaze.com/people/577/castcredits
583;http://api.tvmaze.com/people/583/castcredits
635;http://api.tvmaze.com/people/635/castcredits
688;http://api.tvmaze.com/people/688/castcredits
743;http://api.tvmaze.com/people/743/castcredits
771;http://api.tvmaze.com/people/771/castcredits
811;http://api.tvmaze.com/people/811/castcredits
817;http://api.tvmaze.com/people/817/castcredits
823;http://api.tvmaze.com/people/823/castcredits
864;http://api.tvmaze.com/people/864/castcredits
1008;http://api.tvmaze.com/people/1008/castcredits
1037;http://api.tvmaze.com/people/1037/castcredits
1100;http://api.tvmaze.com/people/1100/castcredits
1238;http://api.tvmaze.com/people/1238/castcredits
1267;http://api.tvmaze.com/people/1267/castcredits
1314;http://api.tvmaze.com/people/1314/castcredits
1358;http://api.tvmaze.com/people/1358/castcredits
1375;http://api.tvmaze.com/people/1375/castcredits
1394;http://api.tvmaze.com/people/1394/castcredits
1555;http://api.tvmaze.com/people/1555/castcredits
1611;http://api.tvmaze.com/people/1611/castcredits
1749;http://api.tvmaze.com/people/1749/castcredits
1813;http://api.tvmaze.com/people/1813/castcredits
2002;http://api.tvmaze.com/people/2002/castcredits
2041;http://api.tvmaze.com/people/2041/castcredits
2101;http://api.tvmaze.com/people/2101/castcredits
2191;http://api.tvmaze.com/people/2191/castcredits
2233;http://api.tvmaze.com/people/2233/castcredits
2280;http://api.tvmaze.com/people/2280/castcredits
2292;http://api.tvmaze.com/people/2292/castcredits
2307;http://api.tvmaze.com/people/2307/castcredits
2339;http://api.tvmaze.com/people/2339/castcredits
2342;http://api.tvmaze.com/people/2342/castcredits
2408;http://api.tvmaze.com/people/2408/castcredits
2428;http://api.tvmaze.com/people/2428/castcredits
2466;http://api.tvmaze.com/people/2466/castcredits
2542;http://api.tvmaze.com/people/2542/castcredits
2545;http://api.tvmaze.com/people/2545/castcredits
2654;http://api.tvmaze.com/people/2654/castcredits
2692;http://api.tvmaze.com/people/2692/castcredits
2735;http://api.tvmaze.com/people/2735/castcredits
2736;http://api.tvmaze.com/people/2736/castcredits
2737;http://api.tvmaze.com/people/2737/castcredits
2743;http://api.tvmaze.com/people/2743/castcredits
2745;http://api.tvmaze.com/people/2745/castcredits
2747;http://api.tvmaze.com/people/2747/castcredits
2963;http://api.tvmaze.com/people/2963/castcredits
2972;http://api.tvmaze.com/people/2972/castcredits
3020;http://api.tvmaze.com/people/3020/castcredits
3032;http://api.tvmaze.com/people/3032/castcredits
3046;http://api.tvmaze.com/people/3046/castcredits
3092;http://api.tvmaze.com/people/3092/castcredits
3261;http://api.tvmaze.com/people/3261/castcredits
3302;http://api.tvmaze.com/people/3302/castcredits
3346;http://api.tvmaze.com/people/3346/castcredits
3428;http://api.tvmaze.com/people/3428/castcredits
3454;http://api.tvmaze.com/people/3454/castcredits
3464;http://api.tvmaze.com/people/3464/castcredits
3505;http://api.tvmaze.com/people/3505/castcredits
3581;http://api.tvmaze.com/people/3581/castcredits
3777;http://api.tvmaze.com/people/3777/castcredits
3841;http://api.tvmaze.com/people/3841/castcredits
3878;http://api.tvmaze.com/people/3878/castcredits
3909;http://api.tvmaze.com/people/3909/castcredits
3942;http://api.tvmaze.com/people/3942/castcredits
3944;http://api.tvmaze.com/people/3944/castcredits
3946;http://api.tvmaze.com/people/3946/castcredits
3948;http://api.tvmaze.com/people/3948/castcredits
3950;http://api.tvmaze.com/people/3950/castcredits
3952;http://api.tvmaze.com/people/3952/castcredits
3954;http://api.tvmaze.com/people/3954/castcredits
3965;http://api.tvmaze.com/people/3965/castcredits
4046;http://api.tvmaze.com/people/4046/castcredits
4193;http://api.tvmaze.com/people/4193/castcredits
4261;http://api.tvmaze.com/people/4261/castcredits
4276;http://api.tvmaze.com/people/4276/castcredits
4379;http://api.tvmaze.com/people/4379/castcredits
4395;http://api.tvmaze.com/people/4395/castcredits
4406;http://api.tvmaze.com/people/4406/castcredits
4445;http://api.tvmaze.com/people/4445/castcredits
4492;http://api.tvmaze.com/people/4492/castcredits
4548;http://api.tvmaze.com/people/4548/castcredits
4585;http://api.tvmaze.com/people/4585/castcredits
4601;http://api.tvmaze.com/people/4601/castcredits
4677;http://api.tvmaze.com/people/4677/castcredits
4820;http://api.tvmaze.com/people/4820/castcredits
4841;http://api.tvmaze.com/people/4841/castcredits
4851;http://api.tvmaze.com/people/4851/castcredits
4908;http://api.tvmaze.com/people/4908/castcredits
4911;http://api.tvmaze.com/people/4911/castcredits
4941;http://api.tvmaze.com/people/4941/castcredits
4983;http://api.tvmaze.com/people/4983/castcredits
5010;http://api.tvmaze.com/people/5010/castcredits
5013;http://api.tvmaze.com/people/5013/castcredits
5019;http://api.tvmaze.com/people/5019/castcredits
5051;http://api.tvmaze.com/people/5051/castcredits
5066;http://api.tvmaze.com/people/5066/castcredits
5198;http://api.tvmaze.com/people/5198/castcredits
5205;http://api.tvmaze.com/people/5205/castcredits
5208;http://api.tvmaze.com/people/5208/castcredits
5209;http://api.tvmaze.com/people/5209/castcredits
5210;http://api.tvmaze.com/people/5210/castcredits
5211;http://api.tvmaze.com/people/5211/castcredits
5212;http://api.tvmaze.com/people/5212/castcredits
5233;http://api.tvmaze.com/people/5233/castcredits
5277;http://api.tvmaze.com/people/5277/castcredits
5425;http://api.tvmaze.com/people/5425/castcredits
5450;http://api.tvmaze.com/people/5450/castcredits
5584;http://api.tvmaze.com/people/5584/castcredits
5615;http://api.tvmaze.com/people/5615/castcredits
5622;http://api.tvmaze.com/people/5622/castcredits
5758;http://api.tvmaze.com/people/5758/castcredits
5760;http://api.tvmaze.com/people/5760/castcredits
5777;http://api.tvmaze.com/people/5777/castcredits
5885;http://api.tvmaze.com/people/5885/castcredits
5934;http://api.tvmaze.com/people/5934/castcredits
5936;http://api.tvmaze.com/people/5936/castcredits
5950;http://api.tvmaze.com/people/5950/castcredits
5952;http://api.tvmaze.com/people/5952/castcredits
5954;http://api.tvmaze.com/people/5954/castcredits
5992;http://api.tvmaze.com/people/5992/castcredits
5994;http://api.tvmaze.com/people/5994/castcredits
5996;http://api.tvmaze.com/people/5996/castcredits
5998;http://api.tvmaze.com/people/5998/castcredits
6000;http://api.tvmaze.com/people/6000/castcredits
6002;http://api.tvmaze.com/people/6002/castcredits
6004;http://api.tvmaze.com/people/6004/castcredits
6006;http://api.tvmaze.com/people/6006/castcredits
6008;http://api.tvmaze.com/people/6008/castcredits
6010;http://api.tvmaze.com/people/6010/castcredits
6012;http://api.tvmaze.com/people/6012/castcredits
6014;http://api.tvmaze.com/people/6014/castcredits
6016;http://api.tvmaze.com/people/6016/castcredits
6018;http://api.tvmaze.com/people/6018/castcredits
6071;http://api.tvmaze.com/people/6071/castcredits
6130;http://api.tvmaze.com/people/6130/castcredits
6158;http://api.tvmaze.com/people/6158/castcredits
6185;http://api.tvmaze.com/people/6185/castcredits
6197;http://api.tvmaze.com/people/6197/castcredits
6199;http://api.tvmaze.com/people/6199/castcredits
6201;http://api.tvmaze.com/people/6201/castcredits
6203;http://api.tvmaze.com/people/6203/castcredits
6205;http://api.tvmaze.com/people/6205/castcredits
6207;http://api.tvmaze.com/people/6207/castcredits
6463;http://api.tvmaze.com/people/6463/castcredits
6471;http://api.tvmaze.com/people/6471/castcredits
6642;http://api.tvmaze.com/people/6642/castcredits
6693;http://api.tvmaze.com/people/6693/castcredits
6719;http://api.tvmaze.com/people/6719/castcredits
6741;http://api.tvmaze.com/people/6741/castcredits
6801;http://api.tvmaze.com/people/6801/castcredits
6943;http://api.tvmaze.com/people/6943/castcredits
7063;http://api.tvmaze.com/people/7063/castcredits
7251;http://api.tvmaze.com/people/7251/castcredits
7282;http://api.tvmaze.com/people/7282/castcredits
7320;http://api.tvmaze.com/people/7320/castcredits
7574;http://api.tvmaze.com/people/7574/castcredits
7591;http://api.tvmaze.com/people/7591/castcredits
7686;http://api.tvmaze.com/people/7686/castcredits
7730;http://api.tvmaze.com/people/7730/castcredits
7740;http://api.tvmaze.com/people/7740/castcredits
7815;http://api.tvmaze.com/people/7815/castcredits
7834;http://api.tvmaze.com/people/7834/castcredits
7848;http://api.tvmaze.com/people/7848/castcredits
7876;http://api.tvmaze.com/people/7876/castcredits
7950;http://api.tvmaze.com/people/7950/castcredits
8071;http://api.tvmaze.com/people/8071/castcredits
8081;http://api.tvmaze.com/people/8081/castcredits
8089;http://api.tvmaze.com/people/8089/castcredits
8171;http://api.tvmaze.com/people/8171/castcredits
8218;http://api.tvmaze.com/people/8218/castcredits
8382;http://api.tvmaze.com/people/8382/castcredits
8450;http://api.tvmaze.com/people/8450/castcredits
8497;http://api.tvmaze.com/people/8497/castcredits
8524;http://api.tvmaze.com/people/8524/castcredits
8525;http://api.tvmaze.com/people/8525/castcredits
8527;http://api.tvmaze.com/people/8527/castcredits
8560;http://api.tvmaze.com/people/8560/castcredits
8574;http://api.tvmaze.com/people/8574/castcredits
8581;http://api.tvmaze.com/people/8581/castcredits
8585;http://api.tvmaze.com/people/8585/castcredits
8587;http://api.tvmaze.com/people/8587/castcredits
8589;http://api.tvmaze.com/people/8589/castcredits
8590;http://api.tvmaze.com/people/8590/castcredits
8591;http://api.tvmaze.com/people/8591/castcredits
8593;http://api.tvmaze.com/people/8593/castcredits
8692;http://api.tvmaze.com/people/8692/castcredits
8747;http://api.tvmaze.com/people/8747/castcredits
8752;http://api.tvmaze.com/people/8752/castcredits
8789;http://api.tvmaze.com/people/8789/castcredits
8848;http://api.tvmaze.com/people/8848/castcredits
9010;http://api.tvmaze.com/people/9010/castcredits
9037;http://api.tvmaze.com/people/9037/castcredits
9162;http://api.tvmaze.com/people/9162/castcredits
9212;http://api.tvmaze.com/people/9212/castcredits
9217;http://api.tvmaze.com/people/9217/castcredits
9219;http://api.tvmaze.com/people/9219/castcredits
9221;http://api.tvmaze.com/people/9221/castcredits
9223;http://api.tvmaze.com/people/9223/castcredits
9225;http://api.tvmaze.com/people/9225/castcredits
9237;http://api.tvmaze.com/people/9237/castcredits
9286;http://api.tvmaze.com/people/9286/castcredits
9317;http://api.tvmaze.com/people/9317/castcredits
9320;http://api.tvmaze.com/people/9320/castcredits
9321;http://api.tvmaze.com/people/9321/castcredits
9322;http://api.tvmaze.com/people/9322/castcredits
9323;http://api.tvmaze.com/people/9323/castcredits
9324;http://api.tvmaze.com/people/9324/castcredits
9325;http://api.tvmaze.com/people/9325/castcredits
9326;http://api.tvmaze.com/people/9326/castcredits
9327;http://api.tvmaze.com/people/9327/castcredits
9328;http://api.tvmaze.com/people/9328/castcredits
9329;http://api.tvmaze.com/people/9329/castcredits
9330;http://api.tvmaze.com/people/9330/castcredits
9331;http://api.tvmaze.com/people/9331/castcredits
9332;http://api.tvmaze.com/people/9332/castcredits
9334;http://api.tvmaze.com/people/9334/castcredits
9335;http://api.tvmaze.com/people/9335/castcredits
9336;http://api.tvmaze.com/people/9336/castcredits
9355;http://api.tvmaze.com/people/9355/castcredits
9666;http://api.tvmaze.com/people/9666/castcredits
9754;http://api.tvmaze.com/people/9754/castcredits
9772;http://api.tvmaze.com/people/9772/castcredits
9801;http://api.tvmaze.com/people/9801/castcredits
9851;http://api.tvmaze.com/people/9851/castcredits
9891;http://api.tvmaze.com/people/9891/castcredits
9913;http://api.tvmaze.com/people/9913/castcredits
9929;http://api.tvmaze.com/people/9929/castcredits
9931;http://api.tvmaze.com/people/9931/castcredits
9933;http://api.tvmaze.com/people/9933/castcredits
9935;http://api.tvmaze.com/people/9935/castcredits
9937;http://api.tvmaze.com/people/9937/castcredits
9939;http://api.tvmaze.com/people/9939/castcredits
9948;http://api.tvmaze.com/people/9948/castcredits
9971;http://api.tvmaze.com/people/9971/castcredits
10118;http://api.tvmaze.com/people/10118/castcredits
10119;http://api.tvmaze.com/people/10119/castcredits
10120;http://api.tvmaze.com/people/10120/castcredits
10121;http://api.tvmaze.com/people/10121/castcredits
10122;http://api.tvmaze.com/people/10122/castcredits
10123;http://api.tvmaze.com/people/10123/castcredits
10124;http://api.tvmaze.com/people/10124/castcredits
10125;http://api.tvmaze.com/people/10125/castcredits
10126;http://api.tvmaze.com/people/10126/castcredits
10127;http://api.tvmaze.com/people/10127/castcredits
137;http://api.tvmaze.com/people/137/castcredits
145;http://api.tvmaze.com/people/145/castcredits
211;http://api.tvmaze.com/people/211/castcredits
214;http://api.tvmaze.com/people/214/castcredits
315;http://api.tvmaze.com/people/315/castcredits
368;http://api.tvmaze.com/people/368/castcredits
416;http://api.tvmaze.com/people/416/castcredits
473;http://api.tvmaze.com/people/473/castcredits
557;http://api.tvmaze.com/people/557/castcredits
577;http://api.tvmaze.com/people/577/castcredits
583;http://api.tvmaze.com/people/583/castcredits
635;http://api.tvmaze.com/people/635/castcredits
688;http://api.tvmaze.com/people/688/castcredits
743;http://api.tvmaze.com/people/743/castcredits
771;http://api.tvmaze.com/people/771/castcredits
10148;http://api.tvmaze.com/people/10148/castcredits
10149;http://api.tvmaze.com/people/10149/castcredits
10162;http://api.tvmaze.com/people/10162/castcredits
10177;http://api.tvmaze.com/people/10177/castcredits
10213;http://api.tvmaze.com/people/10213/castcredits
10271;http://api.tvmaze.com/people/10271/castcredits
10283;http://api.tvmaze.com/people/10283/castcredits
10309;http://api.tvmaze.com/people/10309/castcredits
10333;http://api.tvmaze.com/people/10333/castcredits
10355;http://api.tvmaze.com/people/10355/castcredits
10383;http://api.tvmaze.com/people/10383/castcredits
10390;http://api.tvmaze.com/people/10390/castcredits
10455;http://api.tvmaze.com/people/10455/castcredits
10462;http://api.tvmaze.com/people/10462/castcredits
10477;http://api.tvmaze.com/people/10477/castcredits
10513;http://api.tvmaze.com/people/10513/castcredits
10529;http://api.tvmaze.com/people/10529/castcredits
10532;http://api.tvmaze.com/people/10532/castcredits
10534;http://api.tvmaze.com/people/10534/castcredits
10616;http://api.tvmaze.com/people/10616/castcredits
10647;http://api.tvmaze.com/people/10647/castcredits
10659;http://api.tvmaze.com/people/10659/castcredits
10720;http://api.tvmaze.com/people/10720/castcredits
10862;http://api.tvmaze.com/people/10862/castcredits
10864;http://api.tvmaze.com/people/10864/castcredits
10865;http://api.tvmaze.com/people/10865/castcredits
10866;http://api.tvmaze.com/people/10866/castcredits
10867;http://api.tvmaze.com/people/10867/castcredits
10868;http://api.tvmaze.com/people/10868/castcredits
10970;http://api.tvmaze.com/people/10970/castcredits
11039;http://api.tvmaze.com/people/11039/castcredits
11047;http://api.tvmaze.com/people/11047/castcredits
11079;http://api.tvmaze.com/people/11079/castcredits
11150;http://api.tvmaze.com/people/11150/castcredits
11178;http://api.tvmaze.com/people/11178/castcredits
11312;http://api.tvmaze.com/people/11312/castcredits
11402;http://api.tvmaze.com/people/11402/castcredits
11453;http://api.tvmaze.com/people/11453/castcredits
11457;http://api.tvmaze.com/people/11457/castcredits
11493;http://api.tvmaze.com/people/11493/castcredits
11546;http://api.tvmaze.com/people/11546/castcredits
11547;http://api.tvmaze.com/people/11547/castcredits
11548;http://api.tvmaze.com/people/11548/castcredits
11549;http://api.tvmaze.com/people/11549/castcredits
11550;http://api.tvmaze.com/people/11550/castcredits
11551;http://api.tvmaze.com/people/11551/castcredits
11593;http://api.tvmaze.com/people/11593/castcredits
11621;http://api.tvmaze.com/people/11621/castcredits
11759;http://api.tvmaze.com/people/11759/castcredits
11790;http://api.tvmaze.com/people/11790/castcredits
11952;http://api.tvmaze.com/people/11952/castcredits
11971;http://api.tvmaze.com/people/11971/castcredits
11989;http://api.tvmaze.com/people/11989/castcredits
12043;http://api.tvmaze.com/people/12043/castcredits
12066;http://api.tvmaze.com/people/12066/castcredits
12096;http://api.tvmaze.com/people/12096/castcredits
12097;http://api.tvmaze.com/people/12097/castcredits
12098;http://api.tvmaze.com/people/12098/castcredits
12099;http://api.tvmaze.com/people/12099/castcredits
12100;http://api.tvmaze.com/people/12100/castcredits
12101;http://api.tvmaze.com/people/12101/castcredits
12102;http://api.tvmaze.com/people/12102/castcredits
12141;http://api.tvmaze.com/people/12141/castcredits
12146;http://api.tvmaze.com/people/12146/castcredits
12147;http://api.tvmaze.com/people/12147/castcredits
12148;http://api.tvmaze.com/people/12148/castcredits
12149;http://api.tvmaze.com/people/12149/castcredits
12150;http://api.tvmaze.com/people/12150/castcredits
12201;http://api.tvmaze.com/people/12201/castcredits
12379;http://api.tvmaze.com/people/12379/castcredits
12445;http://api.tvmaze.com/people/12445/castcredits
12575;http://api.tvmaze.com/people/12575/castcredits
12650;http://api.tvmaze.com/people/12650/castcredits
12666;http://api.tvmaze.com/people/12666/castcredits
12842;http://api.tvmaze.com/people/12842/castcredits
12872;http://api.tvmaze.com/people/12872/castcredits
12929;http://api.tvmaze.com/people/12929/castcredits
13169;http://api.tvmaze.com/people/13169/castcredits
13232;http://api.tvmaze.com/people/13232/castcredits
13262;http://api.tvmaze.com/people/13262/castcredits
13277;http://api.tvmaze.com/people/13277/castcredits
13509;http://api.tvmaze.com/people/13509/castcredits
13513;http://api.tvmaze.com/people/13513/castcredits
13516;http://api.tvmaze.com/people/13516/castcredits
13518;http://api.tvmaze.com/people/13518/castcredits
13519;http://api.tvmaze.com/people/13519/castcredits
13520;http://api.tvmaze.com/people/13520/castcredits
13679;http://api.tvmaze.com/people/13679/castcredits
13760;http://api.tvmaze.com/people/13760/castcredits
13774;http://api.tvmaze.com/people/13774/castcredits
13922;http://api.tvmaze.com/people/13922/castcredits
13957;http://api.tvmaze.com/people/13957/castcredits
13968;http://api.tvmaze.com/people/13968/castcredits
13997;http://api.tvmaze.com/people/13997/castcredits
14057;http://api.tvmaze.com/people/14057/castcredits
14099;http://api.tvmaze.com/people/14099/castcredits
14132;http://api.tvmaze.com/people/14132/castcredits
14159;http://api.tvmaze.com/people/14159/castcredits
14162;http://api.tvmaze.com/people/14162/castcredits
14307;http://api.tvmaze.com/people/14307/castcredits
14309;http://api.tvmaze.com/people/14309/castcredits
14310;http://api.tvmaze.com/people/14310/castcredits
14397;http://api.tvmaze.com/people/14397/castcredits
14436;http://api.tvmaze.com/people/14436/castcredits
14472;http://api.tvmaze.com/people/14472/castcredits
14501;http://api.tvmaze.com/people/14501/castcredits
14525;http://api.tvmaze.com/people/14525/castcredits
14541;http://api.tvmaze.com/people/14541/castcredits
14566;http://api.tvmaze.com/people/14566/castcredits
14572;http://api.tvmaze.com/people/14572/castcredits
14625;http://api.tvmaze.com/people/14625/castcredits
14627;http://api.tvmaze.com/people/14627/castcredits
14671;http://api.tvmaze.com/people/14671/castcredits
14672;http://api.tvmaze.com/people/14672/castcredits
14673;http://api.tvmaze.com/people/14673/castcredits
14674;http://api.tvmaze.com/people/14674/castcredits
14853;http://api.tvmaze.com/people/14853/castcredits
14864;http://api.tvmaze.com/people/14864/castcredits
14872;http://api.tvmaze.com/people/14872/castcredits
14960;http://api.tvmaze.com/people/14960/castcredits
14977;http://api.tvmaze.com/people/14977/castcredits
14996;http://api.tvmaze.com/people/14996/castcredits
15000;http://api.tvmaze.com/people/15000/castcredits
15021;http://api.tvmaze.com/people/15021/castcredits
15028;http://api.tvmaze.com/people/15028/castcredits
15029;http://api.tvmaze.com/people/15029/castcredits
15032;http://api.tvmaze.com/people/15032/castcredits
15036;http://api.tvmaze.com/people/15036/castcredits
15043;http://api.tvmaze.com/people/15043/castcredits
15048;http://api.tvmaze.com/people/15048/castcredits
15074;http://api.tvmaze.com/people/15074/castcredits
15098;http://api.tvmaze.com/people/15098/castcredits
15108;http://api.tvmaze.com/people/15108/castcredits
15114;http://api.tvmaze.com/people/15114/castcredits
15128;http://api.tvmaze.com/people/15128/castcredits
15147;http://api.tvmaze.com/people/15147/castcredits
15150;http://api.tvmaze.com/people/15150/castcredits
15172;http://api.tvmaze.com/people/15172/castcredits
15188;http://api.tvmaze.com/people/15188/castcredits
15189;http://api.tvmaze.com/people/15189/castcredits
15196;http://api.tvmaze.com/people/15196/castcredits
15219;http://api.tvmaze.com/people/15219/castcredits
15220;http://api.tvmaze.com/people/15220/castcredits
15234;http://api.tvmaze.com/people/15234/castcredits
15257;http://api.tvmaze.com/people/15257/castcredits
15270;http://api.tvmaze.com/people/15270/castcredits
15276;http://api.tvmaze.com/people/15276/castcredits
15279;http://api.tvmaze.com/people/15279/castcredits
15281;http://api.tvmaze.com/people/15281/castcredits
15285;http://api.tvmaze.com/people/15285/castcredits
15292;http://api.tvmaze.com/people/15292/castcredits
15295;http://api.tvmaze.com/people/15295/castcredits
15315;http://api.tvmaze.com/people/15315/castcredits
15344;http://api.tvmaze.com/people/15344/castcredits
15369;http://api.tvmaze.com/people/15369/castcredits
15371;http://api.tvmaze.com/people/15371/castcredits
15426;http://api.tvmaze.com/people/15426/castcredits
15443;http://api.tvmaze.com/people/15443/castcredits
15448;http://api.tvmaze.com/people/15448/castcredits
15449;http://api.tvmaze.com/people/15449/castcredits
15450;http://api.tvmaze.com/people/15450/castcredits
15451;http://api.tvmaze.com/people/15451/castcredits
15454;http://api.tvmaze.com/people/15454/castcredits
15473;http://api.tvmaze.com/people/15473/castcredits
15474;http://api.tvmaze.com/people/15474/castcredits
15496;http://api.tvmaze.com/people/15496/castcredits
15509;http://api.tvmaze.com/people/15509/castcredits
15520;http://api.tvmaze.com/people/15520/castcredits
15534;http://api.tvmaze.com/people/15534/castcredits
15535;http://api.tvmaze.com/people/15535/castcredits
15574;http://api.tvmaze.com/people/15574/castcredits
15587;http://api.tvmaze.com/people/15587/castcredits
15613;http://api.tvmaze.com/people/15613/castcredits
15630;http://api.tvmaze.com/people/15630/castcredits
15631;http://api.tvmaze.com/people/15631/castcredits
15643;http://api.tvmaze.com/people/15643/castcredits
15644;http://api.tvmaze.com/people/15644/castcredits
15645;http://api.tvmaze.com/people/15645/castcredits
15647;http://api.tvmaze.com/people/15647/castcredits
15654;http://api.tvmaze.com/people/15654/castcredits
15676;http://api.tvmaze.com/people/15676/castcredits
15677;http://api.tvmaze.com/people/15677/castcredits
15678;http://api.tvmaze.com/people/15678/castcredits
15688;http://api.tvmaze.com/people/15688/castcredits
15689;http://api.tvmaze.com/people/15689/castcredits
15693;http://api.tvmaze.com/people/15693/castcredits
15698;http://api.tvmaze.com/people/15698/castcredits
15701;http://api.tvmaze.com/people/15701/castcredits
15706;http://api.tvmaze.com/people/15706/castcredits
15716;http://api.tvmaze.com/people/15716/castcredits
15731;http://api.tvmaze.com/people/15731/castcredits
15751;http://api.tvmaze.com/people/15751/castcredits
15767;http://api.tvmaze.com/people/15767/castcredits
15771;http://api.tvmaze.com/people/15771/castcredits
15778;http://api.tvmaze.com/people/15778/castcredits
15812;http://api.tvmaze.com/people/15812/castcredits
15824;http://api.tvmaze.com/people/15824/castcredits
15826;http://api.tvmaze.com/people/15826/castcredits
15847;http://api.tvmaze.com/people/15847/castcredits
15855;http://api.tvmaze.com/people/15855/castcredits
15866;http://api.tvmaze.com/people/15866/castcredits
15882;http://api.tvmaze.com/people/15882/castcredits
15909;http://api.tvmaze.com/people/15909/castcredits
15928;http://api.tvmaze.com/people/15928/castcredits
16316;http://api.tvmaze.com/people/16316/castcredits
16317;http://api.tvmaze.com/people/16317/castcredits
16331;http://api.tvmaze.com/people/16331/castcredits
16332;http://api.tvmaze.com/people/16332/castcredits
16340;http://api.tvmaze.com/people/16340/castcredits
16341;http://api.tvmaze.com/people/16341/castcredits
16343;http://api.tvmaze.com/people/16343/castcredits
16348;http://api.tvmaze.com/people/16348/castcredits
16352;http://api.tvmaze.com/people/16352/castcredits
16362;http://api.tvmaze.com/people/16362/castcredits
16363;http://api.tvmaze.com/people/16363/castcredits
16367;http://api.tvmaze.com/people/16367/castcredits
16368;http://api.tvmaze.com/people/16368/castcredits
16383;http://api.tvmaze.com/people/16383/castcredits
16386;http://api.tvmaze.com/people/16386/castcredits
16387;http://api.tvmaze.com/people/16387/castcredits
16394;http://api.tvmaze.com/people/16394/castcredits
16395;http://api.tvmaze.com/people/16395/castcredits
16398;http://api.tvmaze.com/people/16398/castcredits
16399;http://api.tvmaze.com/people/16399/castcredits
16400;http://api.tvmaze.com/people/16400/castcredits
16404;http://api.tvmaze.com/people/16404/castcredits
16407;http://api.tvmaze.com/people/16407/castcredits
16417;http://api.tvmaze.com/people/16417/castcredits
16424;http://api.tvmaze.com/people/16424/castcredits
16435;http://api.tvmaze.com/people/16435/castcredits
16440;http://api.tvmaze.com/people/16440/castcredits
16442;http://api.tvmaze.com/people/16442/castcredits
16443;http://api.tvmaze.com/people/16443/castcredits
16448;http://api.tvmaze.com/people/16448/castcredits
16449;http://api.tvmaze.com/people/16449/castcredits
16469;http://api.tvmaze.com/people/16469/castcredits
16470;http://api.tvmaze.com/people/16470/castcredits
16473;http://api.tvmaze.com/people/16473/castcredits
16474;http://api.tvmaze.com/people/16474/castcredits
16475;http://api.tvmaze.com/people/16475/castcredits
16476;http://api.tvmaze.com/people/16476/castcredits
16481;http://api.tvmaze.com/people/16481/castcredits
16492;http://api.tvmaze.com/people/16492/castcredits
16493;http://api.tvmaze.com/people/16493/castcredits
16496;http://api.tvmaze.com/people/16496/castcredits
16497;http://api.tvmaze.com/people/16497/castcredits
16506;http://api.tvmaze.com/people/16506/castcredits
16507;http://api.tvmaze.com/people/16507/castcredits
16510;http://api.tvmaze.com/people/16510/castcredits
16511;http://api.tvmaze.com/people/16511/castcredits
16512;http://api.tvmaze.com/people/16512/castcredits
16514;http://api.tvmaze.com/people/16514/castcredits
16515;http://api.tvmaze.com/people/16515/castcredits
16522;http://api.tvmaze.com/people/16522/castcredits
16523;http://api.tvmaze.com/people/16523/castcredits
16528;http://api.tvmaze.com/people/16528/castcredits
16534;http://api.tvmaze.com/people/16534/castcredits
16535;http://api.tvmaze.com/people/16535/castcredits
16539;http://api.tvmaze.com/people/16539/castcredits
16540;http://api.tvmaze.com/people/16540/castcredits
16542;http://api.tvmaze.com/people/16542/castcredits
16547;http://api.tvmaze.com/people/16547/castcredits
16548;http://api.tvmaze.com/people/16548/castcredits
16549;http://api.tvmaze.com/people/16549/castcredits
16551;http://api.tvmaze.com/people/16551/castcredits
16552;http://api.tvmaze.com/people/16552/castcredits
16559;http://api.tvmaze.com/people/16559/castcredits
16563;http://api.tvmaze.com/people/16563/castcredits
16564;http://api.tvmaze.com/people/16564/castcredits
16567;http://api.tvmaze.com/people/16567/castcredits
16568;http://api.tvmaze.com/people/16568/castcredits
16569;http://api.tvmaze.com/people/16569/castcredits
16574;http://api.tvmaze.com/people/16574/castcredits
16578;http://api.tvmaze.com/people/16578/castcredits
16579;http://api.tvmaze.com/people/16579/castcredits
16580;http://api.tvmaze.com/people/16580/castcredits
16586;http://api.tvmaze.com/people/16586/castcredits
16587;http://api.tvmaze.com/people/16587/castcredits
16588;http://api.tvmaze.com/people/16588/castcredits
16593;http://api.tvmaze.com/people/16593/castcredits
16596;http://api.tvmaze.com/people/16596/castcredits
16597;http://api.tvmaze.com/people/16597/castcredits
16598;http://api.tvmaze.com/people/16598/castcredits
16601;http://api.tvmaze.com/people/16601/castcredits
16602;http://api.tvmaze.com/people/16602/castcredits
16605;http://api.tvmaze.com/people/16605/castcredits
16617;http://api.tvmaze.com/people/16617/castcredits
16618;http://api.tvmaze.com/people/16618/castcredits
16626;http://api.tvmaze.com/people/16626/castcredits
16627;http://api.tvmaze.com/people/16627/castcredits
16628;http://api.tvmaze.com/people/16628/castcredits
16633;http://api.tvmaze.com/people/16633/castcredits
16634;http://api.tvmaze.com/people/16634/castcredits
16635;http://api.tvmaze.com/people/16635/castcredits
16642;http://api.tvmaze.com/people/16642/castcredits
16643;http://api.tvmaze.com/people/16643/castcredits
16649;http://api.tvmaze.com/people/16649/castcredits
16651;http://api.tvmaze.com/people/16651/castcredits
16657;http://api.tvmaze.com/people/16657/castcredits
16664;http://api.tvmaze.com/people/16664/castcredits
16665;http://api.tvmaze.com/people/16665/castcredits
16666;http://api.tvmaze.com/people/16666/castcredits
16670;http://api.tvmaze.com/people/16670/castcredits
16671;http://api.tvmaze.com/people/16671/castcredits
16672;http://api.tvmaze.com/people/16672/castcredits
16678;http://api.tvmaze.com/people/16678/castcredits
16680;http://api.tvmaze.com/people/16680/castcredits
16685;http://api.tvmaze.com/people/16685/castcredits
16690;http://api.tvmaze.com/people/16690/castcredits
16691;http://api.tvmaze.com/people/16691/castcredits
16692;http://api.tvmaze.com/people/16692/castcredits
16696;http://api.tvmaze.com/people/16696/castcredits
16697;http://api.tvmaze.com/people/16697/castcredits
16698;http://api.tvmaze.com/people/16698/castcredits
16704;http://api.tvmaze.com/people/16704/castcredits
16705;http://api.tvmaze.com/people/16705/castcredits
16706;http://api.tvmaze.com/people/16706/castcredits
16708;http://api.tvmaze.com/people/16708/castcredits
16712;http://api.tvmaze.com/people/16712/castcredits
16718;http://api.tvmaze.com/people/16718/castcredits
16719;http://api.tvmaze.com/people/16719/castcredits
16720;http://api.tvmaze.com/people/16720/castcredits
16726;http://api.tvmaze.com/people/16726/castcredits
16727;http://api.tvmaze.com/people/16727/castcredits
16737;http://api.tvmaze.com/people/16737/castcredits
16738;http://api.tvmaze.com/people/16738/castcredits
16741;http://api.tvmaze.com/people/16741/castcredits
16751;http://api.tvmaze.com/people/16751/castcredits
16753;http://api.tvmaze.com/people/16753/castcredits
16759;http://api.tvmaze.com/people/16759/castcredits
16766;http://api.tvmaze.com/people/16766/castcredits
16769;http://api.tvmaze.com/people/16769/castcredits
16771;http://api.tvmaze.com/people/16771/castcredits
16787;http://api.tvmaze.com/people/16787/castcredits
16788;http://api.tvmaze.com/people/16788/castcredits
16797;http://api.tvmaze.com/people/16797/castcredits
16798;http://api.tvmaze.com/people/16798/castcredits
16802;http://api.tvmaze.com/people/16802/castcredits
16803;http://api.tvmaze.com/people/16803/castcredits
16804;http://api.tvmaze.com/people/16804/castcredits
16808;http://api.tvmaze.com/people/16808/castcredits
16813;http://api.tvmaze.com/people/16813/castcredits
16814;http://api.tvmaze.com/people/16814/castcredits
16817;http://api.tvmaze.com/people/16817/castcredits
16824;http://api.tvmaze.com/people/16824/castcredits
16825;http://api.tvmaze.com/people/16825/castcredits
16826;http://api.tvmaze.com/people/16826/castcredits
16827;http://api.tvmaze.com/people/16827/castcredits
16828;http://api.tvmaze.com/people/16828/castcredits
16834;http://api.tvmaze.com/people/16834/castcredits
16835;http://api.tvmaze.com/people/16835/castcredits
16836;http://api.tvmaze.com/people/16836/castcredits
16837;http://api.tvmaze.com/people/16837/castcredits
16838;http://api.tvmaze.com/people/16838/castcredits
16839;http://api.tvmaze.com/people/16839/castcredits
16844;http://api.tvmaze.com/people/16844/castcredits
16845;http://api.tvmaze.com/people/16845/castcredits
16854;http://api.tvmaze.com/people/16854/castcredits
16858;http://api.tvmaze.com/people/16858/castcredits
16859;http://api.tvmaze.com/people/16859/castcredits
16971;http://api.tvmaze.com/people/16971/castcredits
17044;http://api.tvmaze.com/people/17044/castcredits
17049;http://api.tvmaze.com/people/17049/castcredits
17306;http://api.tvmaze.com/people/17306/castcredits
17334;http://api.tvmaze.com/people/17334/castcredits
17371;http://api.tvmaze.com/people/17371/castcredits
17445;http://api.tvmaze.com/people/17445/castcredits
17470;http://api.tvmaze.com/people/17470/castcredits
17505;http://api.tvmaze.com/people/17505/castcredits
17558;http://api.tvmaze.com/people/17558/castcredits
17588;http://api.tvmaze.com/people/17588/castcredits
17640;http://api.tvmaze.com/people/17640/castcredits
17657;http://api.tvmaze.com/people/17657/castcredits
17719;http://api.tvmaze.com/people/17719/castcredits
17736;http://api.tvmaze.com/people/17736/castcredits
17783;http://api.tvmaze.com/people/17783/castcredits
17818;http://api.tvmaze.com/people/17818/castcredits
17890;http://api.tvmaze.com/people/17890/castcredits
17898;http://api.tvmaze.com/people/17898/castcredits
17954;http://api.tvmaze.com/people/17954/castcredits
18043;http://api.tvmaze.com/people/18043/castcredits
18080;http://api.tvmaze.com/people/18080/castcredits
18086;http://api.tvmaze.com/people/18086/castcredits
18308;http://api.tvmaze.com/people/18308/castcredits
18469;http://api.tvmaze.com/people/18469/castcredits
18508;http://api.tvmaze.com/people/18508/castcredits
18676;http://api.tvmaze.com/people/18676/castcredits
18880;http://api.tvmaze.com/people/18880/castcredits
18882;http://api.tvmaze.com/people/18882/castcredits
19047;http://api.tvmaze.com/people/19047/castcredits
19107;http://api.tvmaze.com/people/19107/castcredits
19194;http://api.tvmaze.com/people/19194/castcredits
19205;http://api.tvmaze.com/people/19205/castcredits
19214;http://api.tvmaze.com/people/19214/castcredits
19228;http://api.tvmaze.com/people/19228/castcredits
19441;http://api.tvmaze.com/people/19441/castcredits
19502;http://api.tvmaze.com/people/19502/castcredits
19504;http://api.tvmaze.com/people/19504/castcredits
19536;http://api.tvmaze.com/people/19536/castcredits
19559;http://api.tvmaze.com/people/19559/castcredits
19606;http://api.tvmaze.com/people/19606/castcredits
19611;http://api.tvmaze.com/people/19611/castcredits
19619;http://api.tvmaze.com/people/19619/castcredits
19640;http://api.tvmaze.com/people/19640/castcredits
19686;http://api.tvmaze.com/people/19686/castcredits
19689;http://api.tvmaze.com/people/19689/castcredits
19690;http://api.tvmaze.com/people/19690/castcredits
19691;http://api.tvmaze.com/people/19691/castcredits
19699;http://api.tvmaze.com/people/19699/castcredits
19769;http://api.tvmaze.com/people/19769/castcredits
19788;http://api.tvmaze.com/people/19788/castcredits
19840;http://api.tvmaze.com/people/19840/castcredits
20084;http://api.tvmaze.com/people/20084/castcredits
20213;http://api.tvmaze.com/people/20213/castcredits
20287;http://api.tvmaze.com/people/20287/castcredits
20289;http://api.tvmaze.com/people/20289/castcredits
20327;http://api.tvmaze.com/people/20327/castcredits
20385;http://api.tvmaze.com/people/20385/castcredits
20402;http://api.tvmaze.com/people/20402/castcredits
20421;http://api.tvmaze.com/people/20421/castcredits
20463;http://api.tvmaze.com/people/20463/castcredits
20468;http://api.tvmaze.com/people/20468/castcredits
20540;http://api.tvmaze.com/people/20540/castcredits
20570;http://api.tvmaze.com/people/20570/castcredits
20580;http://api.tvmaze.com/people/20580/castcredits
20647;http://api.tvmaze.com/people/20647/castcredits
20668;http://api.tvmaze.com/people/20668/castcredits
20671;http://api.tvmaze.com/people/20671/castcredits
20797;http://api.tvmaze.com/people/20797/castcredits
20811;http://api.tvmaze.com/people/20811/castcredits
20812;http://api.tvmaze.com/people/20812/castcredits
20813;http://api.tvmaze.com/people/20813/castcredits
20814;http://api.tvmaze.com/people/20814/castcredits
20822;http://api.tvmaze.com/people/20822/castcredits
20830;http://api.tvmaze.com/people/20830/castcredits
20862;http://api.tvmaze.com/people/20862/castcredits
20886;http://api.tvmaze.com/people/20886/castcredits
20896;http://api.tvmaze.com/people/20896/castcredits
20905;http://api.tvmaze.com/people/20905/castcredits
20907;http://api.tvmaze.com/people/20907/castcredits
20929;http://api.tvmaze.com/people/20929/castcredits
21001;http://api.tvmaze.com/people/21001/castcredits
21003;http://api.tvmaze.com/people/21003/castcredits
21035;http://api.tvmaze.com/people/21035/castcredits
21057;http://api.tvmaze.com/people/21057/castcredits
21062;http://api.tvmaze.com/people/21062/castcredits
21075;http://api.tvmaze.com/people/21075/castcredits
21077;http://api.tvmaze.com/people/21077/castcredits
21078;http://api.tvmaze.com/people/21078/castcredits
21084;http://api.tvmaze.com/people/21084/castcredits
21085;http://api.tvmaze.com/people/21085/castcredits
21128;http://api.tvmaze.com/people/21128/castcredits
21165;http://api.tvmaze.com/people/21165/castcredits
21170;http://api.tvmaze.com/people/21170/castcredits
21171;http://api.tvmaze.com/people/21171/castcredits
21176;http://api.tvmaze.com/people/21176/castcredits
21177;http://api.tvmaze.com/people/21177/castcredits
21181;http://api.tvmaze.com/people/21181/castcredits
21185;http://api.tvmaze.com/people/21185/castcredits
21186;http://api.tvmaze.com/people/21186/castcredits
21189;http://api.tvmaze.com/people/21189/castcredits
21190;http://api.tvmaze.com/people/21190/castcredits
21191;http://api.tvmaze.com/people/21191/castcredits
21192;http://api.tvmaze.com/people/21192/castcredits
21193;http://api.tvmaze.com/people/21193/castcredits
21196;http://api.tvmaze.com/people/21196/castcredits
21198;http://api.tvmaze.com/people/21198/castcredits
21199;http://api.tvmaze.com/people/21199/castcredits
21203;http://api.tvmaze.com/people/21203/castcredits
21204;http://api.tvmaze.com/people/21204/castcredits
21205;http://api.tvmaze.com/people/21205/castcredits
21206;http://api.tvmaze.com/people/21206/castcredits
21208;http://api.tvmaze.com/people/21208/castcredits
21209;http://api.tvmaze.com/people/21209/castcredits
21210;http://api.tvmaze.com/people/21210/castcredits
21212;http://api.tvmaze.com/people/21212/castcredits
21213;http://api.tvmaze.com/people/21213/castcredits
21214;http://api.tvmaze.com/people/21214/castcredits
21218;http://api.tvmaze.com/people/21218/castcredits
21219;http://api.tvmaze.com/people/21219/castcredits
21220;http://api.tvmaze.com/people/21220/castcredits
21222;http://api.tvmaze.com/people/21222/castcredits
21223;http://api.tvmaze.com/people/21223/castcredits
21224;http://api.tvmaze.com/people/21224/castcredits
21225;http://api.tvmaze.com/people/21225/castcredits
21226;http://api.tvmaze.com/people/21226/castcredits
21227;http://api.tvmaze.com/people/21227/castcredits
21228;http://api.tvmaze.com/people/21228/castcredits
21232;http://api.tvmaze.com/people/21232/castcredits
21233;http://api.tvmaze.com/people/21233/castcredits
21234;http://api.tvmaze.com/people/21234/castcredits
21235;http://api.tvmaze.com/people/21235/castcredits
21236;http://api.tvmaze.com/people/21236/castcredits
21237;http://api.tvmaze.com/people/21237/castcredits
21238;http://api.tvmaze.com/people/21238/castcredits
21239;http://api.tvmaze.com/people/21239/castcredits
21242;http://api.tvmaze.com/people/21242/castcredits
21243;http://api.tvmaze.com/people/21243/castcredits
21244;http://api.tvmaze.com/people/21244/castcredits
21245;http://api.tvmaze.com/people/21245/castcredits
21246;http://api.tvmaze.com/people/21246/castcredits
21249;http://api.tvmaze.com/people/21249/castcredits
21250;http://api.tvmaze.com/people/21250/castcredits
21251;http://api.tvmaze.com/people/21251/castcredits
21253;http://api.tvmaze.com/people/21253/castcredits
21254;http://api.tvmaze.com/people/21254/castcredits
21255;http://api.tvmaze.com/people/21255/castcredits
21256;http://api.tvmaze.com/people/21256/castcredits
21262;http://api.tvmaze.com/people/21262/castcredits
21263;http://api.tvmaze.com/people/21263/castcredits
21264;http://api.tvmaze.com/people/21264/castcredits
21265;http://api.tvmaze.com/people/21265/castcredits
21266;http://api.tvmaze.com/people/21266/castcredits
21267;http://api.tvmaze.com/people/21267/castcredits
21270;http://api.tvmaze.com/people/21270/castcredits
21272;http://api.tvmaze.com/people/21272/castcredits
21273;http://api.tvmaze.com/people/21273/castcredits
21275;http://api.tvmaze.com/people/21275/castcredits
21276;http://api.tvmaze.com/people/21276/castcredits
21277;http://api.tvmaze.com/people/21277/castcredits
21278;http://api.tvmaze.com/people/21278/castcredits
21279;http://api.tvmaze.com/people/21279/castcredits
21280;http://api.tvmaze.com/people/21280/castcredits
21281;http://api.tvmaze.com/people/21281/castcredits
21282;http://api.tvmaze.com/people/21282/castcredits
21283;http://api.tvmaze.com/people/21283/castcredits
21284;http://api.tvmaze.com/people/21284/castcredits
21285;http://api.tvmaze.com/people/21285/castcredits
21287;http://api.tvmaze.com/people/21287/castcredits
21288;http://api.tvmaze.com/people/21288/castcredits
21289;http://api.tvmaze.com/people/21289/castcredits
21290;http://api.tvmaze.com/people/21290/castcredits
21291;http://api.tvmaze.com/people/21291/castcredits
21292;http://api.tvmaze.com/people/21292/castcredits
21293;http://api.tvmaze.com/people/21293/castcredits
21294;http://api.tvmaze.com/people/21294/castcredits
21295;http://api.tvmaze.com/people/21295/castcredits
21296;http://api.tvmaze.com/people/21296/castcredits
21297;http://api.tvmaze.com/people/21297/castcredits
21298;http://api.tvmaze.com/people/21298/castcredits
21299;http://api.tvmaze.com/people/21299/castcredits
21300;http://api.tvmaze.com/people/21300/castcredits
21301;http://api.tvmaze.com/people/21301/castcredits
21303;http://api.tvmaze.com/people/21303/castcredits
21304;http://api.tvmaze.com/people/21304/castcredits
21305;http://api.tvmaze.com/people/21305/castcredits
21307;http://api.tvmaze.com/people/21307/castcredits
21308;http://api.tvmaze.com/people/21308/castcredits
21313;http://api.tvmaze.com/people/21313/castcredits
21314;http://api.tvmaze.com/people/21314/castcredits
21317;http://api.tvmaze.com/people/21317/castcredits
21319;http://api.tvmaze.com/people/21319/castcredits
21320;http://api.tvmaze.com/people/21320/castcredits
21321;http://api.tvmaze.com/people/21321/castcredits
21322;http://api.tvmaze.com/people/21322/castcredits
21323;http://api.tvmaze.com/people/21323/castcredits
21327;http://api.tvmaze.com/people/21327/castcredits
21328;http://api.tvmaze.com/people/21328/castcredits
21329;http://api.tvmaze.com/people/21329/castcredits
21330;http://api.tvmaze.com/people/21330/castcredits
21331;http://api.tvmaze.com/people/21331/castcredits
21332;http://api.tvmaze.com/people/21332/castcredits
21335;http://api.tvmaze.com/people/21335/castcredits
21338;http://api.tvmaze.com/people/21338/castcredits
21339;http://api.tvmaze.com/people/21339/castcredits
21342;http://api.tvmaze.com/people/21342/castcredits
21343;http://api.tvmaze.com/people/21343/castcredits
21344;http://api.tvmaze.com/people/21344/castcredits
21345;http://api.tvmaze.com/people/21345/castcredits
21410;http://api.tvmaze.com/people/21410/castcredits
21416;http://api.tvmaze.com/people/21416/castcredits
21420;http://api.tvmaze.com/people/21420/castcredits
21435;http://api.tvmaze.com/people/21435/castcredits
21438;http://api.tvmaze.com/people/21438/castcredits
21447;http://api.tvmaze.com/people/21447/castcredits
21449;http://api.tvmaze.com/people/21449/castcredits
21458;http://api.tvmaze.com/people/21458/castcredits
21459;http://api.tvmaze.com/people/21459/castcredits
21465;http://api.tvmaze.com/people/21465/castcredits
21466;http://api.tvmaze.com/people/21466/castcredits
21467;http://api.tvmaze.com/people/21467/castcredits
21468;http://api.tvmaze.com/people/21468/castcredits
21481;http://api.tvmaze.com/people/21481/castcredits
21488;http://api.tvmaze.com/people/21488/castcredits
21501;http://api.tvmaze.com/people/21501/castcredits
21502;http://api.tvmaze.com/people/21502/castcredits
21509;http://api.tvmaze.com/people/21509/castcredits
21510;http://api.tvmaze.com/people/21510/castcredits
21519;http://api.tvmaze.com/people/21519/castcredits
21520;http://api.tvmaze.com/people/21520/castcredits
21521;http://api.tvmaze.com/people/21521/castcredits
21522;http://api.tvmaze.com/people/21522/castcredits
21523;http://api.tvmaze.com/people/21523/castcredits
21534;http://api.tvmaze.com/people/21534/castcredits
21535;http://api.tvmaze.com/people/21535/castcredits
21539;http://api.tvmaze.com/people/21539/castcredits
21545;http://api.tvmaze.com/people/21545/castcredits
21549;http://api.tvmaze.com/people/21549/castcredits
21572;http://api.tvmaze.com/people/21572/castcredits
21581;http://api.tvmaze.com/people/21581/castcredits
21584;http://api.tvmaze.com/people/21584/castcredits
21585;http://api.tvmaze.com/people/21585/castcredits
21592;http://api.tvmaze.com/people/21592/castcredits
21599;http://api.tvmaze.com/people/21599/castcredits
21609;http://api.tvmaze.com/people/21609/castcredits
21614;http://api.tvmaze.com/people/21614/castcredits
21622;http://api.tvmaze.com/people/21622/castcredits
21623;http://api.tvmaze.com/people/21623/castcredits
21627;http://api.tvmaze.com/people/21627/castcredits
21628;http://api.tvmaze.com/people/21628/castcredits
21642;http://api.tvmaze.com/people/21642/castcredits
21643;http://api.tvmaze.com/people/21643/castcredits
21644;http://api.tvmaze.com/people/21644/castcredits
21645;http://api.tvmaze.com/people/21645/castcredits
21648;http://api.tvmaze.com/people/21648/castcredits
21649;http://api.tvmaze.com/people/21649/castcredits
21652;http://api.tvmaze.com/people/21652/castcredits
21653;http://api.tvmaze.com/people/21653/castcredits
21666;http://api.tvmaze.com/people/21666/castcredits
21667;http://api.tvmaze.com/people/21667/castcredits
21671;http://api.tvmaze.com/people/21671/castcredits
21674;http://api.tvmaze.com/people/21674/castcredits
21695;http://api.tvmaze.com/people/21695/castcredits
21709;http://api.tvmaze.com/people/21709/castcredits
21710;http://api.tvmaze.com/people/21710/castcredits
21719;http://api.tvmaze.com/people/21719/castcredits
21720;http://api.tvmaze.com/people/21720/castcredits
21721;http://api.tvmaze.com/people/21721/castcredits
21739;http://api.tvmaze.com/people/21739/castcredits
21744;http://api.tvmaze.com/people/21744/castcredits
21745;http://api.tvmaze.com/people/21745/castcredits
21752;http://api.tvmaze.com/people/21752/castcredits
21755;http://api.tvmaze.com/people/21755/castcredits
21756;http://api.tvmaze.com/people/21756/castcredits
21770;http://api.tvmaze.com/people/21770/castcredits
21776;http://api.tvmaze.com/people/21776/castcredits
21783;http://api.tvmaze.com/people/21783/castcredits
21789;http://api.tvmaze.com/people/21789/castcredits
21790;http://api.tvmaze.com/people/21790/castcredits
21799;http://api.tvmaze.com/people/21799/castcredits
21806;http://api.tvmaze.com/people/21806/castcredits
21807;http://api.tvmaze.com/people/21807/castcredits
21821;http://api.tvmaze.com/people/21821/castcredits
21822;http://api.tvmaze.com/people/21822/castcredits
21836;http://api.tvmaze.com/people/21836/castcredits
21840;http://api.tvmaze.com/people/21840/castcredits
21841;http://api.tvmaze.com/people/21841/castcredits
21842;http://api.tvmaze.com/people/21842/castcredits
21844;http://api.tvmaze.com/people/21844/castcredits
21845;http://api.tvmaze.com/people/21845/castcredits
21854;http://api.tvmaze.com/people/21854/castcredits
21855;http://api.tvmaze.com/people/21855/castcredits
21872;http://api.tvmaze.com/people/21872/castcredits
21873;http://api.tvmaze.com/people/21873/castcredits
21874;http://api.tvmaze.com/people/21874/castcredits
21883;http://api.tvmaze.com/people/21883/castcredits
21884;http://api.tvmaze.com/people/21884/castcredits
21885;http://api.tvmaze.com/people/21885/castcredits
21886;http://api.tvmaze.com/people/21886/castcredits
21901;http://api.tvmaze.com/people/21901/castcredits
21902;http://api.tvmaze.com/people/21902/castcredits
21905;http://api.tvmaze.com/people/21905/castcredits
21906;http://api.tvmaze.com/people/21906/castcredits
21911;http://api.tvmaze.com/people/21911/castcredits
21919;http://api.tvmaze.com/people/21919/castcredits
21920;http://api.tvmaze.com/people/21920/castcredits
21925;http://api.tvmaze.com/people/21925/castcredits
21928;http://api.tvmaze.com/people/21928/castcredits
21930;http://api.tvmaze.com/people/21930/castcredits
21936;http://api.tvmaze.com/people/21936/castcredits
21939;http://api.tvmaze.com/people/21939/castcredits
21942;http://api.tvmaze.com/people/21942/castcredits
21943;http://api.tvmaze.com/people/21943/castcredits
22007;http://api.tvmaze.com/people/22007/castcredits
22059;http://api.tvmaze.com/people/22059/castcredits
22063;http://api.tvmaze.com/people/22063/castcredits
22064;http://api.tvmaze.com/people/22064/castcredits
22069;http://api.tvmaze.com/people/22069/castcredits
22076;http://api.tvmaze.com/people/22076/castcredits
22078;http://api.tvmaze.com/people/22078/castcredits
22086;http://api.tvmaze.com/people/22086/castcredits
22095;http://api.tvmaze.com/people/22095/castcredits
22099;http://api.tvmaze.com/people/22099/castcredits
22104;http://api.tvmaze.com/people/22104/castcredits
22146;http://api.tvmaze.com/people/22146/castcredits
22147;http://api.tvmaze.com/people/22147/castcredits
22172;http://api.tvmaze.com/people/22172/castcredits
22176;http://api.tvmaze.com/people/22176/castcredits
22193;http://api.tvmaze.com/people/22193/castcredits
22198;http://api.tvmaze.com/people/22198/castcredits
22199;http://api.tvmaze.com/people/22199/castcredits
22204;http://api.tvmaze.com/people/22204/castcredits
22211;http://api.tvmaze.com/people/22211/castcredits
22214;http://api.tvmaze.com/people/22214/castcredits
22215;http://api.tvmaze.com/people/22215/castcredits
22222;http://api.tvmaze.com/people/22222/castcredits
22223;http://api.tvmaze.com/people/22223/castcredits
22243;http://api.tvmaze.com/people/22243/castcredits
22245;http://api.tvmaze.com/people/22245/castcredits
22250;http://api.tvmaze.com/people/22250/castcredits
22321;http://api.tvmaze.com/people/22321/castcredits
22395;http://api.tvmaze.com/people/22395/castcredits
22458;http://api.tvmaze.com/people/22458/castcredits
22522;http://api.tvmaze.com/people/22522/castcredits
22536;http://api.tvmaze.com/people/22536/castcredits
22697;http://api.tvmaze.com/people/22697/castcredits
22792;http://api.tvmaze.com/people/22792/castcredits
22798;http://api.tvmaze.com/people/22798/castcredits
22803;http://api.tvmaze.com/people/22803/castcredits
22899;http://api.tvmaze.com/people/22899/castcredits
23153;http://api.tvmaze.com/people/23153/castcredits
23177;http://api.tvmaze.com/people/23177/castcredits
23198;http://api.tvmaze.com/people/23198/castcredits
23204;http://api.tvmaze.com/people/23204/castcredits
23262;http://api.tvmaze.com/people/23262/castcredits
File diff suppressed because it is too large Load Diff