45 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
wjones 1283bd282a Refactor README.md for clarity and detail: enhance project description, add features, usage examples, and troubleshooting sections; remove outdated mongo2pgsql.py and search_mongodb.py files 2026-02-06 12:23:03 -05:00
wjones 9fbd5dc8b9 Refactor MongoDB to PostgreSQL data synchronization: remove json2postgres.py, add mongodb2postgres.py for improved schema generation and data insertion 2026-02-06 12:11:46 -05:00
wjones a52bd5571e Refactor database metadata initialization and update JSON download logic for improved clarity and efficiency 2026-02-06 10:36:20 -05:00
wjones a49354bbcb add search_mongodb.py and change seriesid in update_mongodb.py from a string to an int. 2026-02-06 08:44:37 -05:00
wjones 66cb23bb6d file not executable 2026-02-05 16:58:30 -05:00
wjones 0f0f9078c6 Refactor load_json_to_mongodb function to include updateDict for change detection and enhance logging with screenprint method 2026-02-05 16:53:10 -05:00
wjones 7e98f01cd2 commit gitignore file 2026-02-05 16:12:09 -05:00
wjones c6038c0b01 Add /cache/aliases/*.json to .gitignore 2026-02-05 14:10:45 -05:00
wjones 2d414ecbfc updated gitignore file 2026-02-05 14:00:47 -05:00
wjones 332ad8e5d8 Enhance load_json_to_mongodb function to support change detection and statistics tracking for inserted and updated documents. 2026-02-05 10:37:17 -05:00
wjones df89aad7e6 Add MongoDB integration and JSON download functionality
- Created a new script `update_mongodb.py` to handle downloading JSON data from APIs and loading it into MongoDB.
- Implemented functions for downloading JSON files, loading data into MongoDB collections, and managing updates.
- Updated `update_json.py` to instantiate the `dbmongo` class for MongoDB operations.
- Enhanced logging for better tracking of the download and load processes.
2026-02-05 09:02:37 -05:00
wjones bbf92af79f update gitignore file 2026-02-05 07:31:43 -05:00
wjones 26a7e94b4e Update previous updates file naming to include current timestamp for versioning 2026-01-22 14:53:40 -05:00
wjones 968a3a2949 Refactor update table logging and processing; enhance clarity in output messages. 2026-01-22 14:50:30 -05:00
wjones f6254ee42e Update psycopg2 dependency to psycopg2-binary in requirements.txt; refine timestamp handling in main function of update_mongo.py; add new cache.tgz file. 2026-01-22 14:25:53 -05:00
wjones f5627e58a8 Add function to update update table 2026-01-22 12:36:47 -05:00
wjones 5c97eb3fb4 Add skip_ids functionality to main for conditional JSON downloads 2025-12-13 14:21:53 -05:00
wjones f79f8772ae Update .gitignore to include additional log files and temporary directories for better cleanup 2025-12-12 14:21:34 -05:00
wjones 99a2d78032 Comment out unused code in CreateTempTables and update_tvupdates methods for future verification; streamline set_apienv function by removing jget parameter and related logic. 2025-12-12 13:48:05 -05:00
wjones 3447ba369f Remove unused jprocessor module files to streamline the codebase. 2025-12-12 11:22:30 -05:00
wjones eb1615ea3d Remove unused tableddl_from_yaml.py and its test file to clean up the repository. 2025-12-12 11:04:45 -05:00
wjones 4f0267ecf2 Refactor update logic in Dbexec class; adjust logging level in settings; enhance JSON downloader function call in update_mongo.py for improved error handling. 2025-12-12 07:46:45 -05:00
wjones 097225c942 Add logging support to JSON download function in update_mongo.py 2025-12-12 07:37:50 -05:00
wjones 24203aff66 Comment out SQL update logic in Dbexec class for verification; adjust sleep duration in JSON downloader to reduce server load; enhance logging in update_mongo.py for clarity on processing updates. 2025-12-12 07:37:19 -05:00
wjones 3bc35a08f9 Fix typo in JSON download message to reflect the number of files instead of series IDs 2025-12-12 07:15:11 -05:00
wjones 7dfe22fe71 Add guest cast and crew directories to cache structure; update file check logic 2025-12-12 07:13:40 -05:00
wjones e975b947fa Refactor update_mongo.py to enhance JSON downloading and file handling; update MongoDB host in test_tableddl_from_yaml.py 2025-12-11 17:53:41 -05:00
wjones a18c6a7386 Add PostgreSQL virtual table extension and SQLite utilities
- Implement pg_vtab as a minimal SQLite virtual table module backed by PostgreSQL.
- Create sqlite2pgsql utility for copying tables from SQLite to PostgreSQL.
- Add sqlite_pg_plugin to expose PostgreSQL tables to SQLite.
- Include Makefile for building the pg_vtab shared object.
- Update README with usage instructions and limitations.
- Remove obsolete cache.zip file.
2025-12-11 18:45:49 +00:00
wjones 2c2d6da194 Merge branch 'main' of https://gitea.wpjones.com/wjones/new_dbsync 2025-12-11 15:23:13 +00:00
wjones 96bd323567 Implement bulk upsert functionality in dbmongo class and update configuration for MongoDB database 2025-12-11 15:20:50 +00:00
wjones 197a81d8fb Refactor dbmongo class to improve code structure and readability 2025-12-09 16:28:35 -05:00
wjones dfeb5cb36e Add timestamp conversion for updated fields in dbmongo class and clean up json_downloader.py 2025-12-09 16:00:25 -05:00
wjones aa2001bc87 Merge branch 'main' of https://gitea.wpjones.com/wjones/new_dbsync 2025-12-09 13:01:25 -05:00
wjones c5502711e7 Refactor update_mongo.py for improved readability and maintainability 2025-12-09 13:00:48 -05:00
wjones c51150396c Refactor json_downloader.py to correct directory usage for JSON downloads 2025-11-26 08:07:25 -05:00
wjones b390fc9f9a Add function to read configuration from file or directory
- Introduced `read_config_file` function to handle reading configuration settings.
- Supports both direct file paths and project root directory structure.
- Extracts various database and update type settings into a dictionary.
- Handles timestamp calculations for update types.
2025-11-25 08:13:59 -05:00
wjones 7837ca2dd4 Remove unused functions and clean up json_downloader.py for better maintainability 2025-11-17 11:37:46 -05:00
wjones 271bf606ca Refactor code structure for improved readability and maintainability 2025-11-14 16:24:06 -05:00
wjones 1d14e97637 Merge pull request 'removing cache folder and view and json files are no longer used.' (#1) from development into main
Reviewed-on: #1
2025-06-27 15:46:19 -04:00
wjones b9f135e325 removing cache folder and view and json files are no longer used. 2025-06-27 15:08:11 -04:00
34 changed files with 345943 additions and 1277 deletions
+14 -2
View File
@@ -5,11 +5,23 @@
/.vs/new_dbsync/FileContentIndex /.vs/new_dbsync/FileContentIndex
/.vs /.vs
*.pyc *.pyc
*.json /cache/series/*.json
/cache/episodes/*.json
/cache/cast/*.json
/cache/crew/*.json
/cache/guestcast/*.json
/cache/guestcrew/*.json
/cache/aliases/*.json
*.DS_Store *.DS_Store
*.log *.log
/pyproject.toml /pyproject.toml
dbsync.log.* dbsync.log.*
.idea .idea
/log /log
venv /venv
/temp/post_tvmaze_updates.json
/temp/post_tvmaze_updates.json.*
/temp/tvmaze_updates.json
/temp.skip_ids.txt
/archived
settings/tvsync_settings.cfg
+159 -5
View File
@@ -1,8 +1,162 @@
# DBSyncer # DBSyncer - MongoDB to PostgreSQL Data Synchronization
This project has two parts: A comprehensive data synchronization tool that:
1.) it uses the tvmaze API to download Television dat and store it in a mongodb database. 1. Downloads Television data from the TVMaze API and stores it in MongoDB
2.) Takes saved json data and inserts/updates data in a postgresql database. 2. Automatically generates PostgreSQL table schemas from MongoDB document structures
3. Efficiently inserts/updates MongoDB documents into PostgreSQL with intelligent type conversion
## Features
[Learn more about creating GitLab projects.](https://docs.gitlab.com/ee/gitlab-basics/create-project.html) ### Schema Generation (`db/schema_generator.py`)
- **Automatic Schema Inference**: Analyzes MongoDB documents to infer PostgreSQL types
- **Type Mapping**: Intelligent conversion between MongoDB and PostgreSQL types:
- Objects/Arrays → JSONB
- Dates → TIMESTAMP
- Numbers → BIGINT / DOUBLE PRECISION
- Strings → TEXT
### Document Insertion
- **Batch Processing**: Efficiently inserts documents in configurable batches (default: 1000)
- **Type Conversion**: Automatic type conversion for:
- Epoch timestamps ↔ ISO datetime strings
- JSON strings ↔ Python dict/list
- ObjectId → Text
- **UPSERT Support**: Uses PostgreSQL `ON CONFLICT ... DO UPDATE SET` for automatic upserts
- **Error Handling**: Detailed logging with per-row error reporting
## Quick Start
### Prerequisites
```bash
pip install -r requirements.txt
```
### Configuration
Edit `settings/tvsync_settings.cfg`:
```ini
[dbsettings]
hostname = localhost
pgsqlport = 5432
pgsqlusername = postgres
pgsqlpassword = your_password
dbname = media_dbsync
mghostname = localhost
mgport = 27017
mgdbname = media
[database]
dbtype = pgsql
updateschema = updates
```
### Main Scripts
#### `update_mongodb.py`
Fetches TV data from TVMaze API and stores in MongoDB:
```bash
python3 update_mongodb.py
```
#### `mongodb2postgres.py`
Generates PostgreSQL schemas from MongoDB collections and inserts documents:
```bash
python3 mongodb2postgres.py
```
This script will:
1. Analyze each MongoDB collection (series, episodes, actors, characters, crew)
2. Automatically create corresponding PostgreSQL tables in the `updates` schema
3. Insert/upsert all documents with proper type conversions
4. Report progress and any errors
## Usage Examples
### Generate Table Schema from MongoDB Collection
```python
from db.schema_generator import MongoToPostgresSchemaGenerator
generator = MongoToPostgresSchemaGenerator(sample_size=100)
schema = generator.analyze_collection(mongo_db, 'series')
# View the SQL
sql = generator.generate_create_table_sql('seriesdata', schema_name='dbo', pk_field='id')
print(sql)
# Create the table
generator.create_table_in_postgres(engine, 'seriesdata', schema_name='dbo', pk_field='id')
```
### Insert Documents with Type Conversion
```python
from db.schema_generator import MongoDocumentInserter
inserter = MongoDocumentInserter(batch_size=1000)
# From a collection
count = inserter.insert_from_collection(
engine=engine,
table_name='seriesdata',
collection=mongo_db['series'],
schema_name='dbo',
on_conflict="ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, updated=EXCLUDED.updated",
)
print(f"Inserted {count} documents")
```
## Architecture
### Database Classes (`db/functions.py`)
- `settype`: Manages database engine creation and API configuration
- `dbmongo`: Handles MongoDB connections and operations
- `Dbexec`: Executes raw SQL queries and batch updates
### Schema Generator (`db/schema_generator.py`)
- `MongoToPostgresSchemaGenerator`: Infers and creates table schemas
- `MongoDocumentInserter`: Handles document insertion with type conversion
### API Integration (`api/`)
- TVMaze API client for fetching show, episode, cast, and crew data
- TheTVDB API client (optional)
## Performance Optimizations
- **Batch Inserts**: Uses SQLAlchemy's parameterized queries for efficient batch operations
- **Transactional Inserts**: Groups inserts in transactions to reduce database overhead
- **Column Type Awareness**: Converts data to match target column types, avoiding casting overhead
- **Connection Pooling**: Reuses database connections (pool_size=20, max_overflow=20)
- **UPSERT Operations**: Uses PostgreSQL's `ON CONFLICT` for atomic insert-or-update
## Troubleshooting
### MetaData Schema Argument Error
If you see `sqlalchemy.exc.ArgumentError: Could not parse SQLAlchemy URL`, ensure:
- Use `MetaData(schema="updates")` instead of `MetaData("updates")`
- Pass full connection strings to `create_engine()`, not schema names
### Type Conversion Errors
The inserter automatically handles:
- Datetime strings → epoch timestamps (for BIGINT columns)
- Epoch ints → datetime objects (for TIMESTAMP columns)
- JSON strings → Python dicts (for JSONB columns)
If type errors persist, check that:
1. Table schema matches MongoDB document fields
2. Column types are correctly inferred from sample documents
3. Use appropriate `on_conflict` clauses for upserts
### Column Name Case Sensitivity
PostgreSQL stores unquoted identifiers as lowercase. The inserter automatically:
- Lowercases column names in `ON CONFLICT` clauses
- Uses case-insensitive matching when mapping MongoDB fields to PostgreSQL columns
## Contributing
When modifying:
- **Schema inference**: Update `MongoToPostgresSchemaGenerator.TYPE_MAPPING` for new type support
- **Type conversion**: Extend `MongoDocumentInserter.convert_value()` for custom conversions
- **Database operations**: Use parameterized queries via `text()` to prevent SQL injection
- **Error handling**: Add specific exception types to improve debugging
## License
See LICENSE file for details.
BIN
View File
Binary file not shown.
+89 -89
View File
@@ -1,89 +1,89 @@
import os import os
import progressbar import progressbar
from pathlib import Path from pathlib import Path
from time import sleep from time import sleep
from tqdm import tqdm from tqdm import tqdm
class Cacher(object): class Cacher(object):
def __init__(self): def __init__(self):
pass pass
def process(self, options, jget, new_updates, ROOTDIR, URLS): def process(self, options, jget, new_updates, ROOTDIR, URLS):
SERIESCACHE = str(ROOTDIR) + '/cache/series/' SERIESCACHE = str(ROOTDIR) + '/cache/series/'
EPISODECACHE = str(ROOTDIR) + '/cache/episodes/' EPISODECACHE = str(ROOTDIR) + '/cache/episodes/'
CASTCACHE = str(ROOTDIR) + '/cache/cast/' CASTCACHE = str(ROOTDIR) + '/cache/cast/'
CREWCACHE = str(ROOTDIR) + '/cache/crew/' CREWCACHE = str(ROOTDIR) + '/cache/crew/'
TEMPDIR = ROOTDIR + '/temp/' TEMPDIR = ROOTDIR + '/temp/'
available_updates = {} available_updates = {}
updateList = [] updateList = []
if options['refreshcache'] == '1': if options['refreshcache'] == '1':
print('Refreshing cache directory.') print('Refreshing cache directory.')
print('Checking cache directory for missing files.') print('Checking cache directory for missing files.')
for update in new_updates: for update in new_updates:
available_updates[update[0]] = update[1] available_updates[update[0]] = update[1]
updateList = [str(key) for key in available_updates.keys()] updateList = [str(key) for key in available_updates.keys()]
for series in tqdm(range(len(new_updates)), desc='Checking cache', unit='series'): for series in tqdm(range(len(new_updates)), desc='Checking cache', unit='series'):
seriesid = new_updates[series][0] seriesid = new_updates[series][0]
cachefile = str(seriesid) + '.json' cachefile = str(seriesid) + '.json'
seriescachefile = f'{SERIESCACHE}{cachefile}' seriescachefile = f'{SERIESCACHE}{cachefile}'
episodecachefile = f'{EPISODECACHE}{cachefile}' episodecachefile = f'{EPISODECACHE}{cachefile}'
castcachefile = f'{CASTCACHE}{cachefile}' castcachefile = f'{CASTCACHE}{cachefile}'
crewcachefile = f'{CREWCACHE}{cachefile}' crewcachefile = f'{CREWCACHE}{cachefile}'
self.check_and_download(jget, URLS['showurl'], str(seriesid), new_updates[0], seriescachefile, 1500) self.check_and_download(jget, URLS['showurl'], str(seriesid), new_updates[0], seriescachefile, 1500)
sleep(1) sleep(1)
self.check_and_download(jget, URLS['episodesurl'], str(seriesid), new_updates[0], episodecachefile, 1500) self.check_and_download(jget, URLS['episodesurl'], str(seriesid), new_updates[0], episodecachefile, 1500)
sleep(1) sleep(1)
self.check_and_download(jget, URLS['casturl'], str(seriesid), new_updates[0], castcachefile, 500) self.check_and_download(jget, URLS['casturl'], str(seriesid), new_updates[0], castcachefile, 500)
sleep(1) sleep(1)
self.check_and_download(jget, URLS['crewurl'], str(seriesid), new_updates[0], crewcachefile, 500) self.check_and_download(jget, URLS['crewurl'], str(seriesid), new_updates[0], crewcachefile, 500)
sleep(1) sleep(1)
print('Refresh completed.') print('Refresh completed.')
def check_and_download(self, jget, url, SERIESID, series_id, cachefile, size_limit): def check_and_download(self, jget, url, SERIESID, series_id, cachefile, size_limit):
try: try:
my_abs_path = Path(cachefile).resolve(strict=True) my_abs_path = Path(cachefile).resolve(strict=True)
file_stats = os.stat(cachefile) file_stats = os.stat(cachefile)
if file_stats.st_size <= size_limit: if file_stats.st_size <= size_limit:
#print(f'File Size for {cachefile} in Bytes is {file_stats.st_size}. File too small. Attempting to download.') #print(f'File Size for {cachefile} in Bytes is {file_stats.st_size}. File too small. Attempting to download.')
jget.fetch_json(url.replace(SERIESID, str(series_id)), series_id, cachefile) jget.fetch_json(url.replace(SERIESID, str(series_id)), series_id, cachefile)
except FileNotFoundError: except FileNotFoundError:
#print(f'{cachefile} does not exist. Attempting to download.') #print(f'{cachefile} does not exist. Attempting to download.')
jget.fetch_json(url.replace(SERIESID, str(series_id)), series_id, cachefile) jget.fetch_json(url.replace(SERIESID, str(series_id)), series_id, cachefile)
except Exception as e: except Exception as e:
print(f'Unable to download {cachefile}. {e}') print(f'Unable to download {cachefile}. {e}')
def update_cache(self, jget, updateList, ROOTDIR, URLS, options, lprint): def update_cache(self, jget, updateList, ROOTDIR, URLS, options, lprint):
if options['skipcache'] == '0': if options['skipcache'] == '0':
SERIESCACHE = str(ROOTDIR) + '/cache/series/' SERIESCACHE = str(ROOTDIR) + '/cache/series/'
EPISODECACHE = str(ROOTDIR) + '/cache/episodes/' EPISODECACHE = str(ROOTDIR) + '/cache/episodes/'
CASTCACHE = str(ROOTDIR) + '/cache/cast/' CASTCACHE = str(ROOTDIR) + '/cache/cast/'
CREWCACHE = str(ROOTDIR) + '/cache/crew/' CREWCACHE = str(ROOTDIR) + '/cache/crew/'
TEMPDIR = ROOTDIR + '/temp/' TEMPDIR = ROOTDIR + '/temp/'
print('Now downloading updates.') print('Now downloading updates.')
updaterange = len(updateList) updaterange = len(updateList)
for seriesid in tqdm(range(updaterange), desc='Downloading updates', unit='seriesid'): for seriesid in tqdm(range(updaterange), desc='Downloading updates', unit='seriesid'):
cachefile = f'{updateList[seriesid]}.json' cachefile = f'{updateList[seriesid]}.json'
seriescachefile = SERIESCACHE + cachefile seriescachefile = SERIESCACHE + cachefile
episodecachefile = EPISODECACHE + cachefile episodecachefile = EPISODECACHE + cachefile
castcachefile = CASTCACHE + cachefile castcachefile = CASTCACHE + cachefile
crewcachefile = CREWCACHE + cachefile crewcachefile = CREWCACHE + cachefile
self.download_json(jget, URLS['showurl'], updateList[seriesid], seriescachefile, lprint) self.download_json(jget, URLS['showurl'], updateList[seriesid], seriescachefile, lprint)
sleep(1) sleep(1)
self.download_json(jget, URLS['episodesurl'], updateList[seriesid], episodecachefile, lprint) self.download_json(jget, URLS['episodesurl'], updateList[seriesid], episodecachefile, lprint)
sleep(1) sleep(1)
self.download_json(jget, URLS['casturl'], updateList[seriesid], castcachefile, lprint) self.download_json(jget, URLS['casturl'], updateList[seriesid], castcachefile, lprint)
sleep(1) sleep(1)
self.download_json(jget, URLS['crewurl'], updateList[seriesid], crewcachefile, lprint) self.download_json(jget, URLS['crewurl'], updateList[seriesid], crewcachefile, lprint)
sleep(1) sleep(1)
def download_json(self, jget, url, seriesid, cachefile, lprint): def download_json(self, jget, url, seriesid, cachefile, lprint):
try: try:
jget.fetch_json(url.replace('<seriesid>', str(seriesid)), seriesid, cachefile) jget.fetch_json(url.replace('<seriesid>', str(seriesid)), seriesid, cachefile)
except Exception as e: except Exception as e:
lprint.logprint('info', f'Unable to acquire {cachefile}. {e}') lprint.logprint('info', f'Unable to acquire {cachefile}. {e}')
def __del__(self): def __del__(self):
pass pass
+6 -7
View File
@@ -250,7 +250,6 @@ class CreateTempTables():
pass pass
self.create(Base, engine, metadata, session, self.tablelist) self.create(Base, engine, metadata, session, self.tablelist)
session.commit() session.commit()
#self.send(tablelist)
def settables(self, metadata, engine): def settables(self, metadata, engine):
tablelist = [] tablelist = []
@@ -379,12 +378,12 @@ class CreateTempTables():
schema = "updates", schema = "updates",
extend_existing=True) extend_existing=True)
tablelist.append(countries) tablelist.append(countries)
tvupdates = Table('tvupdates', metadata, # tvupdates = Table('tvupdates', metadata,
Column('seriesid', BIGINT, primary_key = True), # Column('seriesid', BIGINT, primary_key = True),
Column('timestamp', VARCHAR(15)), # Column('timestamp', VARCHAR(15)),
schema = "updates", # schema = "updates",
extend_existing=True) # extend_existing=True)
tablelist.append(tvupdates) # tablelist.append(tvupdates)
return(tablelist) return(tablelist)
def create(self, Base, engine, metadata, session, tablelist): def create(self, Base, engine, metadata, session, tablelist):
+86 -129
View File
@@ -19,8 +19,9 @@ from db.pgsql.Merger import Episodemerge as PGEpisodemerge
import api.tvmaze.episodes import api.tvmaze.episodes
import api.tvmaze.cast import api.tvmaze.cast
import api.tvmaze.series import api.tvmaze.series
from pymongo import MongoClient from pymongo import MongoClient, ReplaceOne, InsertOne
from tqdm import tqdm from tqdm import tqdm
from datetime import datetime
Createtables = Createtables Createtables = Createtables
CreateTempTables = CreateTempTables CreateTempTables = CreateTempTables
@@ -113,23 +114,18 @@ class settype(object):
) )
currentDir = os.path.dirname(os.path.abspath(__file__)) currentDir = os.path.dirname(os.path.abspath(__file__))
currentDir = os.path.dirname(currentDir) currentDir = os.path.dirname(currentDir)
ddl_file_path = f"{currentDir}/db/ddl/recreate_updates_schema.sql" # ddl_file_path = f"{currentDir}/db/ddl/recreate_updates_schema.sql"
with open(ddl_file_path, "r") as file: # with open(ddl_file_path, "r") as file:
sql = file.read() # sql = file.read()
file # file
with engine.connect() as conn:
with conn as cursor:
cursor.execute(text(sql))
# conn.commit()
# with engine.connect() as conn: # with engine.connect() as conn:
# conn.execute(text("CREATE SCHEMA IF NOT EXISTS updates")) # with conn as cursor:
# conn.execute(text("CREATE SCHEMA IF NOT EXISTS dbo")) # cursor.execute(text(sql))
# conn.commit()
updatesBase.prepare(engine, reflect=True, schema="updates") updatesBase.prepare(engine, reflect=True, schema="updates")
dboBase.prepare(engine, reflect=True, schema="dbo") dboBase.prepare(engine, reflect=True, schema="dbo")
metadata = sqlalchemy.MetaData("updates") metadata = sqlalchemy.MetaData(schema="updates")
metadata.reflect(bind=engine) metadata.reflect(bind=engine)
dboMetadata = sqlalchemy.MetaData("dbo") dboMetadata = sqlalchemy.MetaData(schema="dbo")
dboMetadata.reflect(bind=engine) dboMetadata.reflect(bind=engine)
session = Session(engine) session = Session(engine)
self.dbengine["engine"] = engine self.dbengine["engine"] = engine
@@ -170,6 +166,25 @@ class dbmongo(object):
global mgcharacters global mgcharacters
global mgcrew 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): def __init__(self, options):
host = options["mghostname"] host = options["mghostname"]
port = options["mgport"] port = options["mgport"]
@@ -210,8 +225,6 @@ class dbmongo(object):
lprint.logprint( lprint.logprint(
"info", f"Unable to open {datatype} file for {seriesid}" "info", f"Unable to open {datatype} file for {seriesid}"
) )
# print(f'Unable to open {datatype} file for seriesid {seriesid}')
return None
def process_series_data( def process_series_data(
self, self,
@@ -233,6 +246,8 @@ class dbmongo(object):
"info", f"Unable to process series file for {seriesid}" "info", f"Unable to process series file for {seriesid}"
) )
else: else:
update_time = self.convert_updated_time(seriesdata["updated"])
seriesdata["updated"] = update_time
self.update_country_data( self.update_country_data(
country, country,
countrydictionary, countrydictionary,
@@ -260,79 +275,17 @@ class dbmongo(object):
dboBase, dboBase,
lprint, lprint,
) )
#self.bulk_upsert_by_id(self.mgseries, seriesdata)
self.insert_or_update_mongo( self.insert_or_update_mongo(
self.mgseries, seriesdata, "id" self.mgseries, seriesdata, "id"
) )
def process_cast_data(self, persondata, characterdata, seriesid, lprint): def process_cast_data(self, persondata, characterdata, seriesid, lprint):
try: self.bulk_upsert_by_id(self.mgactors, persondata)
aupserts = [ self.bulk_upsert_by_id(self.mgcharacters, characterdata)
self.mgactors.update_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in persondata
]
bupserts = [
self.mgcharacters.update_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in characterdata
]
except Exception as e:
print(f"Skipping cast data.Reason: {str(e)}")
else:
[
self.mgactors.update_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in aupserts
]
[
self.mgcharacters.insert_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in bupserts
]
def process_crew_data(self, crewdata, seriesid, jget, lprint): def process_crew_data(self, crewdata, seriesid, jget, lprint):
try: self.bulk_upsert_by_id(self.mgcrew, crewdata)
upserts = [
self.mgcrew.update_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in crewdata
]
except Exception as e:
print(f"Skipping crew data.Reason: {str(e)}")
else:
[
self.mgcrew.insert_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in upserts
]
def process_episode_data(self, episodedata, seriesid, lprint):
try:
upserts = [
self.mgepisodes.update_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in episodedata
]
except Exception as e:
print(
f"file does not contain the correct json. "
f"Skipping. {str(e)}"
)
else:
for episode in upserts:
[
self.mgepisodes.insert_many(
{"id": x["id"]}, {"$setOnInsert": x}, upsert=True
)
for x in upserts
]
def update_country_data( def update_country_data(
self, country, countrydictionary, dbExec, dbengine, dbBase, self, country, countrydictionary, dbExec, dbengine, dbBase,
@@ -374,9 +327,7 @@ class dbmongo(object):
dbExec.rawsql_insert( dbExec.rawsql_insert(
dbengine["engine"], insert_sql, lprint dbengine["engine"], insert_sql, lprint
) )
# seriesresults['countryid'] = 'No Country'
# return seriesresults['countryid']
def update_network_data( def update_network_data(
self, self,
networkchannel, networkchannel,
@@ -426,19 +377,9 @@ class dbmongo(object):
del new_values["$set"]["_id"] del new_values["$set"]["_id"]
collection.update_one(filter_query, new_values) collection.update_one(filter_query, new_values)
def insert_or_update_many_mongo(self, collection, data, unique_key):
try:
collection.insert_many(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( def update_mongo(
self, self,
updateList, updateList,
ROOTDIR,
directories, directories,
jget, jget,
countrydictionary, countrydictionary,
@@ -456,7 +397,7 @@ class dbmongo(object):
failed_file_count = 0 failed_file_count = 0
def process_data( def process_data(
file_path, data_type, process_method, failed_file_count, lprint file_path, data_type, failed_file_count, lprint
): ):
"""Helper function to load and process data.""" """Helper function to load and process data."""
data = self.load_json_file( data = self.load_json_file(
@@ -475,33 +416,25 @@ class dbmongo(object):
persondata = [] persondata = []
characterdata = [] characterdata = []
for item in data: for item in data:
update_time = self.convert_updated_time(item["person"]["updated"])
item["person"]["updated"] = update_time
if "person" in item: if "person" in item:
item["person"]["seriesid"] = seriesid
persondata.append(item["person"]) persondata.append(item["person"])
if "character" in item: if "character" in item:
item["character"]["seriesid"] = seriesid
characterdata.append(item["character"]) characterdata.append(item["character"])
try: try:
for item in persondata: self.bulk_upsert_by_id(self.mgactors, persondata)
item["seriesid"] = seriesid self.bulk_upsert_by_id(self.mgcharacters, characterdata)
for item in characterdata:
item["seriesid"] = seriesid
except Exception as e:
lprint.logprint(
"info", f"Unable to add {data_type} data. {e}"
)
try:
process_method(
persondata, characterdata, seriesid, lprint
)
failed_file_count = "N/A" failed_file_count = "N/A"
return failed_file_count
except Exception as e: except Exception as e:
failed_file_count = 1 failed_file_count = 1
lprint.logprint( lprint.logprint(
"info", "info",
f"Unable to process {data_type} data. {e}" f"Unable to process {data_type} data. {e}"
) )
return failed_file_count
if "crew" in file_path: if "crew" in file_path:
try: try:
crewdata = [] crewdata = []
@@ -512,11 +445,15 @@ class dbmongo(object):
) )
else: else:
for item in data: for item in data:
item["seriesid"] = seriesid 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: if "person" in item:
crewdata.append(item["person"]) crewdata.append(item["person"])
try: try:
process_method(crewdata, seriesid, jget, lprint) self.bulk_upsert_by_id(self.mgcrew, crewdata)
failed_file_count = "N/A" failed_file_count = "N/A"
return failed_file_count return failed_file_count
except Exception as e: except Exception as e:
@@ -526,7 +463,7 @@ class dbmongo(object):
f"Unable to process {data_type} data.{e}" f"Unable to process {data_type} data.{e}"
) )
return failed_file_count return failed_file_count
return data
for series in tqdm( for series in tqdm(
range(len(updateList)), desc="Update MongoDB", unit="series" range(len(updateList)), desc="Update MongoDB", unit="series"
): ):
@@ -552,9 +489,7 @@ class dbmongo(object):
countrydictionary, countrydictionary,
seriesid, seriesid,
) )
self.insert_or_update_mongo( self.bulk_upsert_by_id(self.mgseries, seriesdata)
self.mgseries, seriesdata, "id"
)
except Exception as e: except Exception as e:
series_failed_count += 1 series_failed_count += 1
lprint.logprint( lprint.logprint(
@@ -567,33 +502,55 @@ class dbmongo(object):
else: else:
processed_count += 1 processed_count += 1
# Process episode, cast, and crew data using the helper function
fcount = process_data( fcount = process_data(
episodecachefile, episodecachefile,
"episode", "episode",
self.process_episode_data,
failed_file_count, failed_file_count,
lprint, lprint,
) )
if isinstance(fcount, int): if isinstance(fcount, int):
episode_failed_count = episode_failed_count + fcount 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( fcount = process_data(
castcachefile, "cast", self.process_cast_data, castcachefile, "cast",failed_file_count, lprint
failed_file_count, lprint
) )
if isinstance(fcount, int): if isinstance(fcount, int):
cast_failed_count = cast_failed_count + fcount cast_failed_count = cast_failed_count + fcount
try: if os.path.isfile(crewcachefile):
fcount = process_data( fcount = process_data(
crewcachefile, "crew", self.process_crew_data, crewcachefile, "crew",
failed_file_count, lprint failed_file_count, lprint
) )
except Exception:
crew_failed_count = crew_failed_count + 1
else:
if isinstance(fcount, int): if isinstance(fcount, int):
crew_failed_count = crew_failed_count + fcount crew_failed_count = crew_failed_count + fcount
processed_count += 1 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"Processing complete. {processed_count} series processed.")
print(f"Failed to load {series_failed_count} series files.") print(f"Failed to load {series_failed_count} series files.")
+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,
)
+743
View File
@@ -0,0 +1,743 @@
"""Generate PostgreSQL table schemas from MongoDB document structures."""
__author__ = 'Wendell Jones'
from typing import Any, Dict, List, Optional, Set
from pymongo import MongoClient
from sqlalchemy import text, MetaData, insert, Table
import logging
import json
logger = logging.getLogger(__name__)
class MongoToPostgresSchemaGenerator:
"""Infer PostgreSQL schema from MongoDB collection and create tables."""
# Map MongoDB/Python types to PostgreSQL types
TYPE_MAPPING = {
'null': 'TEXT',
'boolean': 'BOOLEAN',
'integer': 'BIGINT',
'float': 'DOUBLE PRECISION',
'string': 'TEXT',
'array': 'JSONB',
'object': 'JSONB',
'date': 'TIMESTAMP',
'objectid': 'TEXT',
}
def __init__(self, sample_size: int = 100):
"""
Initialize schema generator.
Args:
sample_size: Number of documents to sample for type inference.
"""
self.sample_size = sample_size
self.field_types: Dict[str, Set[str]] = {}
self.final_schema: Dict[str, str] = {}
def infer_type(self, value: Any) -> str:
"""
Infer the MongoDB type of a value.
Args:
value: The value to inspect.
Returns:
A type string suitable for TYPE_MAPPING.
"""
if value is None:
return 'null'
if isinstance(value, bool):
return 'boolean'
if isinstance(value, int) and not isinstance(value, bool):
return 'integer'
if isinstance(value, float):
return 'float'
if isinstance(value, str):
return 'string'
if isinstance(value, list):
return 'array'
if isinstance(value, dict):
return 'object'
# Handle MongoDB-specific types
if hasattr(value, '__class__'):
type_name = value.__class__.__name__.lower()
if 'objectid' in type_name:
return 'objectid'
if 'datetime' in type_name:
return 'date'
return 'string'
def merge_types(self, types: Set[str]) -> str:
"""
Merge multiple observed types for a field into a single PostgreSQL type.
Priority: object/array > string > float > integer > boolean > null
Args:
types: Set of type strings.
Returns:
The merged PostgreSQL type.
"""
if not types:
return self.TYPE_MAPPING['null']
# Priority order
priority = ['array', 'object', 'string', 'float', 'integer', 'boolean', 'null', 'date']
for ptype in priority:
if ptype in types:
return self.TYPE_MAPPING.get(ptype, self.TYPE_MAPPING['string'])
return self.TYPE_MAPPING['string']
def analyze_collection(
self,
mongo_db: Any,
collection_name: Any
) -> Dict[str, str]:
"""
Analyze a MongoDB collection and infer field types.
Args:
mongo_db: A pymongo database object or None if collection_name is a collection.
collection_name: Name of the collection (str) or a pymongo Collection object directly.
Returns:
Dictionary mapping field names to PostgreSQL types.
"""
self.field_types = {}
# Handle both cases: collection name string or collection object
if isinstance(collection_name, str):
collection = mongo_db[collection_name]
else:
# Assume it's already a pymongo Collection object
collection = collection_name
# Sample documents from the collection
sample = list(collection.find({}).limit(self.sample_size))
if not sample:
logger.warning(f"Collection '{collection_name}' is empty; no schema inferred.")
return {}
logger.info(f"Analyzing {len(sample)} documents from '{collection_name}'")
# Gather field types
for doc in sample:
for field_name, value in doc.items():
inferred = self.infer_type(value)
self.field_types.setdefault(field_name, set()).add(inferred)
# Merge types
self.final_schema = {}
for field_name, types in self.field_types.items():
merged_type = self.merge_types(types)
self.final_schema[field_name] = merged_type
logger.debug(f" {field_name}: {merged_type} (observed: {types})")
return self.final_schema
def generate_create_table_sql(
self,
table_name: str,
schema_name: str = 'public',
pk_field: Optional[str] = None,
exclude_fields: Optional[List[str]] = None,
) -> str:
"""
Generate a CREATE TABLE statement from the inferred schema.
Args:
table_name: Name of the table to create.
schema_name: PostgreSQL schema (default: 'public').
pk_field: Field to use as primary key (e.g., 'id', '_id', 'seriesid').
exclude_fields: List of field names to exclude.
Returns:
A CREATE TABLE SQL statement.
"""
if not self.final_schema:
raise ValueError("No schema has been analyzed. Call analyze_collection first.")
exclude_fields = exclude_fields or []
columns = []
for field_name, pg_type in self.final_schema.items():
if field_name in exclude_fields or field_name == '_id':
continue
columns.append(f" {field_name} {pg_type}")
# Add primary key constraint if specified
constraints = []
if pk_field and pk_field in self.final_schema:
constraints.append(f" PRIMARY KEY ({pk_field})")
all_lines = columns + constraints
columns_str = ',\n'.join(all_lines)
full_table_name = f"{schema_name}.{table_name}"
sql = f"""CREATE TABLE IF NOT EXISTS {full_table_name} (
{columns_str}
);"""
return sql
def create_table_in_postgres(
self,
engine: Any,
table_name: str,
schema_name: str = 'public',
pk_field: Optional[str] = None,
exclude_fields: Optional[List[str]] = None,
drop_existing: bool = False,
) -> None:
"""
Create a table in PostgreSQL based on the inferred schema.
Args:
engine: SQLAlchemy engine connected to PostgreSQL, or a dict with 'engine' key.
table_name: Name of the table to create.
schema_name: PostgreSQL schema (default: 'public').
pk_field: Field to use as primary key.
exclude_fields: List of field names to exclude.
drop_existing: If True, drop the table before creating.
"""
# Handle both dict (from db/functions.py) and engine object directly
if isinstance(engine, dict):
engine = engine['engine']
full_table_name = f"{schema_name}.{table_name}"
create_sql = self.generate_create_table_sql(
table_name,
schema_name=schema_name,
pk_field=pk_field,
exclude_fields=exclude_fields,
)
with engine.begin() as conn:
if drop_existing:
logger.info(f"Dropping existing table {full_table_name}...")
conn.execute(text(f"DROP TABLE IF EXISTS {full_table_name};"))
logger.info(f"Creating table {full_table_name}...")
conn.execute(text(create_sql))
logger.info(f"Table {full_table_name} created successfully.")
def create_table_from_collection(
self,
mongo_db: Any,
collection_name: str,
engine: Any,
table_name: Optional[str] = None,
schema_name: str = 'dbo',
pk_field: Optional[str] = None,
exclude_fields: Optional[List[str]] = None,
drop_existing: bool = False,
) -> str:
"""
End-to-end: analyze a MongoDB collection and create a PostgreSQL table.
Args:
mongo_db: A pymongo database object.
collection_name: Name of the MongoDB collection.
engine: SQLAlchemy engine connected to PostgreSQL.
table_name: Name of the PostgreSQL table (defaults to collection_name).
schema_name: PostgreSQL schema (default: 'dbo').
pk_field: Field to use as primary key.
exclude_fields: List of field names to exclude.
drop_existing: If True, drop the table before creating.
Returns:
The generated SQL statement.
"""
if table_name is None:
table_name = collection_name
# Analyze the collection
self.analyze_collection(mongo_db, collection_name)
# Create the table
self.create_table_in_postgres(
engine,
table_name,
schema_name=schema_name,
pk_field=pk_field,
exclude_fields=exclude_fields,
drop_existing=drop_existing,
)
# Return the SQL for reference
return self.generate_create_table_sql(
table_name,
schema_name=schema_name,
pk_field=pk_field,
exclude_fields=exclude_fields,
)
# Convenience function
def create_postgres_table_from_mongo(
mongo_db: Any,
collection_name: str,
engine: Any,
table_name: Optional[str] = None,
schema_name: str = 'dbo',
pk_field: Optional[str] = None,
sample_size: int = 100,
drop_existing: bool = False,
) -> str:
"""
Convenience function to create a PostgreSQL table from a MongoDB collection.
Example:
from db.schema_generator import create_postgres_table_from_mongo
sql = create_postgres_table_from_mongo(
mongo_db=mgdb,
collection_name='series',
engine=pg_engine,
table_name='seriesdata',
schema_name='dbo',
pk_field='seriesid',
drop_existing=True,
)
print(f"Created table with schema:\\n{sql}")
Args:
mongo_db: A pymongo database object.
collection_name: Name of the MongoDB collection.
engine: SQLAlchemy engine connected to PostgreSQL.
table_name: Name of the PostgreSQL table (defaults to collection_name).
schema_name: PostgreSQL schema (default: 'dbo').
pk_field: Field to use as primary key (optional).
sample_size: Number of documents to sample (default: 100).
drop_existing: If True, drop the table before creating.
Returns:
The generated SQL statement.
"""
generator = MongoToPostgresSchemaGenerator(sample_size=sample_size)
return generator.create_table_from_collection(
mongo_db=mongo_db,
collection_name=collection_name,
engine=engine,
table_name=table_name,
schema_name=schema_name,
pk_field=pk_field,
drop_existing=drop_existing,
)
class MongoDocumentInserter:
"""Insert MongoDB documents into PostgreSQL tables with type conversion."""
def __init__(self, batch_size: int = 10000):
"""
Initialize the document inserter.
Args:
batch_size: Number of documents to insert per batch (default: 1000).
"""
self.batch_size = batch_size
def convert_value(self, value: Any) -> Any:
"""
Convert a MongoDB value to PostgreSQL-compatible type.
- MongoDB ObjectId → string
- datetime → string (ISO format)
- list/dict → JSON string (will be stored as JSONB)
- None → None
Args:
value: The value to convert.
Returns:
The converted value.
"""
if value is None:
return None
# Handle MongoDB ObjectId
if hasattr(value, '__class__') and 'ObjectId' in value.__class__.__name__:
return str(value)
# Handle datetime
if hasattr(value, 'isoformat'):
return value.isoformat()
# Handle lists and dicts (will be stored as JSONB)
if isinstance(value, (list, dict)):
return json.dumps(value)
return value
def prepare_documents(
self,
documents: List[Dict[str, Any]],
column_names: List[str],
exclude_fields: Optional[List[str]] = None,
) -> List[Dict[str, Any]]:
"""
Prepare documents for insertion: convert types, exclude fields, etc.
Args:
documents: List of documents (dicts) from MongoDB.
column_names: Column names in the target table.
exclude_fields: Fields to exclude (e.g., ['_id']).
Returns:
List of prepared documents ready for insertion.
"""
exclude_fields = exclude_fields or ['_id']
prepared = []
for doc in documents:
row = {}
for col in column_names:
if col not in exclude_fields and col in doc:
row[col] = self.convert_value(doc[col])
elif col not in exclude_fields:
row[col] = None
prepared.append(row)
return prepared
def insert_documents(
self,
engine: Any,
table_name: str,
documents: List[Dict[str, Any]],
schema_name: str = 'dbo',
on_conflict: Optional[str] = None,
exclude_fields: Optional[List[str]] = None,
skip_null: bool = False,
) -> int:
"""
Insert documents into a PostgreSQL table in batches.
Args:
engine: SQLAlchemy engine (or dict with 'engine' key).
table_name: Target table name.
documents: List of documents (dicts) to insert.
schema_name: PostgreSQL schema (default: 'dbo').
on_conflict: ON CONFLICT clause (e.g., "DO NOTHING" or
"DO UPDATE SET field=EXCLUDED.field").
exclude_fields: Fields to exclude from insert (default: ['_id']).
skip_null: If True, skip None values in the INSERT (uses COALESCE).
Returns:
Total number of documents inserted.
"""
if isinstance(engine, dict):
engine = engine['engine']
if not documents:
logger.warning("No documents to insert.")
return 0
exclude_fields = exclude_fields or ['_id']
full_table_name = f"{schema_name}.{table_name}"
total_inserted = 0
# Reflect target table to get column definitions and types
metadata = MetaData()
try:
table = Table(table_name, metadata, autoload_with=engine, schema=schema_name)
column_objs = {c.name: c for c in table.columns}
column_names = list(column_objs.keys())
except Exception:
# Fallback: derive column names from first document
column_names = [k for k in documents[0].keys() if k not in exclude_fields]
column_objs = {}
# Prepare and convert documents according to target column types
prepared: List[Dict[str, Any]] = []
for doc in documents:
# create case-insensitive key map for doc
key_map = {k.lower(): k for k in doc.keys()}
row: Dict[str, Any] = {}
for col in column_names:
if exclude_fields and col in exclude_fields:
continue
# find matching key in document (case-insensitive)
val = None
if col in doc:
val = doc[col]
elif col.lower() in key_map:
val = doc[key_map[col.lower()]]
# Convert based on column type if available
colobj = column_objs.get(col)
if colobj is not None and val is not None:
try:
from datetime import datetime
import sqlalchemy as sa
ctype = colobj.type
# Integer target: convert ISO datetime strings to epoch
if isinstance(ctype, (sa.Integer, sa.BigInteger)):
if isinstance(val, str):
# try parse datetime string
try:
dt = datetime.fromisoformat(val)
val = int(dt.timestamp())
except Exception:
# try numeric parse
try:
val = int(val)
except Exception:
pass
elif isinstance(val, float):
val = int(val)
# Datetime target: convert epoch ints to datetime
elif isinstance(ctype, (sa.DateTime, sa.TIMESTAMP)):
if isinstance(val, (int, float)):
val = datetime.fromtimestamp(val)
elif isinstance(val, str):
try:
val = datetime.fromisoformat(val)
except Exception:
pass
# JSON target: ensure Python dict/list
elif 'JSON' in type(ctype).__name__.upper() or 'JSON' in str(ctype).upper():
if isinstance(val, str):
try:
val = json.loads(val)
except Exception:
pass
except Exception:
# ignore conversion errors and use original value
pass
# Final conversion for general values
if val is None:
row[col] = None
else:
row[col] = self.convert_value(val) if col not in (exclude_fields or []) else None
prepared.append(row)
# Build INSERT statement template using reflected column order
cols_str = ', '.join(column_names)
placeholders = ', '.join([':' + col for col in column_names])
insert_sql = f"INSERT INTO {full_table_name} ({cols_str}) VALUES ({placeholders})"
if on_conflict:
insert_sql += f" {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:
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:
conn.execute(text(insert_sql), batch)
batch_count = len(batch)
total_inserted += batch_count
logger.info(
f"Inserted {batch_count} documents into {full_table_name} (total: {total_inserted})"
)
except Exception as e:
logger.error(f"Error inserting batch into {full_table_name}: {e}")
raise
return total_inserted
def insert_from_collection(
self,
engine: Any,
table_name: str,
collection: Any,
schema_name: str = 'dbo',
on_conflict: Optional[str] = None,
exclude_fields: Optional[List[str]] = None,
query_filter: Optional[Dict] = None,
limit: Optional[int] = None,
) -> int:
"""
Insert documents from a MongoDB collection into PostgreSQL.
Args:
engine: SQLAlchemy engine (or dict with 'engine' key).
table_name: Target PostgreSQL table name.
collection: PyMongo collection object.
schema_name: PostgreSQL schema (default: 'dbo').
on_conflict: ON CONFLICT clause.
exclude_fields: Fields to exclude (default: ['_id']).
query_filter: MongoDB query filter (default: {}).
limit: Maximum documents to insert (default: None for all).
Returns:
Total number of documents inserted.
"""
query_filter = query_filter or {}
logger.info(f"Fetching documents from MongoDB collection...")
cursor = collection.find(query_filter)
if limit:
cursor = cursor.limit(limit)
documents = list(cursor)
logger.info(f"Retrieved {len(documents)} documents.")
return self.insert_documents(
engine=engine,
table_name=table_name,
documents=documents,
schema_name=schema_name,
on_conflict=on_conflict,
exclude_fields=exclude_fields,
)
# Convenience function for inserting documents
def insert_mongo_documents_to_postgres(
engine: Any,
table_name: str,
documents: List[Dict[str, Any]],
schema_name: str = 'dbo',
on_conflict: Optional[str] = None,
exclude_fields: Optional[List[str]] = None,
batch_size: int = 1000,
) -> int:
"""
Convenience function to insert documents into PostgreSQL.
Example:
from db.schema_generator import insert_mongo_documents_to_postgres
count = insert_mongo_documents_to_postgres(
engine=dbengine,
table_name='seriesdata',
documents=series_docs,
schema_name='dbo',
on_conflict="DO NOTHING",
exclude_fields=['_id'],
)
print(f"Inserted {count} documents")
Args:
engine: SQLAlchemy engine (or dict with 'engine' key).
table_name: Target table name.
documents: List of documents to insert.
schema_name: PostgreSQL schema (default: 'dbo').
on_conflict: ON CONFLICT clause (optional).
exclude_fields: Fields to exclude (default: ['_id']).
batch_size: Number of documents per batch (default: 1000).
Returns:
Total number of documents inserted.
"""
inserter = MongoDocumentInserter(batch_size=batch_size)
return inserter.insert_documents(
engine=engine,
table_name=table_name,
documents=documents,
schema_name=schema_name,
on_conflict=on_conflict,
exclude_fields=exclude_fields,
)
+15 -6
View File
@@ -12,23 +12,32 @@ class Dbexec():
def update_tvupdates(self, engine, available_updates, tablename): def update_tvupdates(self, engine, available_updates, tablename):
print('Creating update transaction.') print('Creating update transaction.')
# Build batch insert data
updatelist = [ updatelist = [
{ {
'seriesid': seriesid, 'seriesid': str(seriesid),
'timestamp': available_updates[seriesid] 'timestamp': available_updates[seriesid]
} }
for seriesid in tqdm( for seriesid in tqdm(
available_updates, available_updates,
desc='Updating tvupdates table', desc='Preparing tvupdates records',
unit='record' unit='record'
) )
] ]
print('Applying updates to table.') print('Applying updates to table.')
with engine.connect() as conn: # Use a transactional context; SQLAlchemy Connection.execute accepts
conn.execute(text(f"truncate table {tablename}")) # a list of parameter mappings for bulk inserts (it will use the DBAPI
conn.execute(tablename.insert(), updatelist) # executemany under the hood).
conn.commit() with engine.begin() as conn:
print(f'Truncating table {tablename} before insert.')
conn.execute(text(f"TRUNCATE TABLE {tablename}"))
print(f'Inserting {len(updatelist)} records into {tablename}.')
if updatelist:
conn.execute(
text(f"INSERT INTO {tablename} (seriesid, timestamp) VALUES (:seriesid, :timestamp)"),
updatelist,
)
print('Update of tvupdates table is complete.') print('Update of tvupdates table is complete.')
def rawsql_select(self, engine, sqlquery, lprint): def rawsql_select(self, engine, sqlquery, lprint):
View File
-105
View File
@@ -1,105 +0,0 @@
__author__ = 'Wendell Jones'
import json
import progressbar
from url.process import Loadurl
class Jsonload():
def __init__(self):
pass
def load(self, rawdata):
jdata = {}
jdata = json.loads(rawdata)
newdata = self.dump(jdata)
return(newdata)
def dump(self, rawdata):
jdata = json.dumps(rawdata, indent=4)
return(jdata)
def jconvert(self, inputdata):
outputdata = json.loads(inputdata)
return(outputdata)
def populate_cache(self, jdata, jfile):
try:
with open(jfile, 'w') as datafile:
datafile.write(jdata)
except:
print('Unable to save cache file.')
else:
datafile.close()
def fetch_json(self, weburl, seriesid, cachefile):
uprocess = Loadurl()
try:
webdata = uprocess.get_data(weburl)
jdata = self.load(webdata)
except Exception as e:
print('Unable to get JSON data for ' + str(seriesid) + ': ' + str(e))
else:
if jdata == '[]':
pass
else:
self.populate_cache(jdata, cachefile)
def processSeriesJson(self, seriesdata):
seriesresults = {}
networkchannel = {}
webchannel = {}
country = {}
seriesresults['id'] = seriesdata['id']
seriesresults['url'] = seriesdata['url']
seriesresults['name'] = seriesdata['name']
seriesresults['type'] = seriesdata['type']
seriesresults['language'] = seriesdata['language']
seriesresults['genres'] = ', '.join(seriesdata['genres'])
seriesresults['status'] = seriesdata['status']
seriesresults['runtime'] = seriesdata['runtime']
seriesresults['premiered'] = seriesdata['premiered']
seriesresults['officalsite'] = seriesdata['officialSite']
if len(seriesdata['schedule']['days']) > 0:
seriesresults['days'] = seriesdata['schedule']['days'][0]
seriesresults['time'] = seriesdata['schedule']['time']
seriesresults['rating'] = seriesdata['rating']['average']
seriesresults['weight'] = seriesdata['weight']
seriesresults['networkid'] = seriesdata['id']
seriesresults['webchannel'] = seriesdata['id']
seriesresults['tvrage'] = seriesdata['externals']['tvrage']
seriesresults['thetvdb'] = seriesdata['externals']['thetvdb']
seriesresults['imdb'] = seriesdata['externals']['imdb']
if seriesdata['image']:
seriesresults['medium'] = seriesdata['image']['medium']
seriesresults['original'] = seriesdata['image']['original']
seriesresults['summary'] = seriesdata['summary']
seriesresults['serieslink'] = seriesdata['_links']['self']['href']
if 'previousepisode' in seriesdata['_links'].keys():
seriesresults['previousepisode'] = seriesdata['_links']['previousepisode']['href']
if 'nextepisode' in seriesdata['_links'].keys():
seriesresults['nextepisode'] = seriesdata['_links']['nextepisode']['href']
if seriesdata['network'] and seriesdata['network'] != None:
networkchannel['id'] = seriesdata['network']['id']
networkchannel['name'] = seriesdata['network']['name']
#networkchannel['countryid'] = country['id']
if seriesdata['network']['country']:
# country['id'] = "auto incremented id"
country['name'] = seriesdata['network']['country']['name']
country['code'] = seriesdata['network']['country']['code']
country['timezone'] = seriesdata['network']['country']['timezone']
if seriesdata['webChannel'] and seriesdata['webChannel'] != None:
webchannel['id'] = seriesdata['webChannel']['id']
webchannel['name'] = seriesdata['webChannel']['name']
#webchannel['countryid'] = country['id']
if seriesdata['webChannel']['country']:
# country['id'] = "auto incremented id"
country['name'] = seriesdata['webchannel']['country']['name']
country['code'] = seriesdata['webchannel']['country']['code']
country['timezone'] = seriesdata['webchannel']['country']['timezone']
return(seriesresults, networkchannel, country, webchannel)
def __del__(self):
pass
-80
View File
@@ -1,80 +0,0 @@
__author__ = 'Wendell Jones'
from ast import Global
import json
import progressbar
class Process_View(object):
global seriesinfo
global episodeinfo
def __init__(self):
pass
def process_seriesdata(self, seriesdata):
seriesinfo = {}
seriesinfo['seriesid'] = seriesdata['id']
seriesinfo['seriesurl'] = seriesdata['url']
seriesinfo['seriesname'] = seriesdata['name']
seriesinfo['seriestype'] = seriesdata['type']
seriesinfo['series_language'] = seriesdata['language']
seriesinfo['series_genres'] = ', '.join(seriesdata['genres'])
seriesinfo['series_status'] = seriesdata['status']
seriesinfo['series_runtime'] = seriesdata['runtime']
seriesinfo['series_premiered'] = seriesdata['premiered']
seriesinfo['offcialsite'] = seriesdata['officialSite']
seriesinfo['air_day'] = ', '.join(seriesdata['schedule']['days'])
seriesinfo['air_time'] = seriesdata['schedule']['time']
seriesinfo['rating'] = seriesdata['rating']['average']
seriesinfo['weight'] = seriesdata['weight']
seriesinfo['networkid'] = seriesdata['network']['id']
seriesinfo['networkname'] = seriesdata['network']['name']
seriesinfo['tvrageid'] = seriesdata['externals']['tvrage']
seriesinfo['tvdbid'] = seriesdata['externals']['thetvdb']
seriesinfo['imdbid'] = seriesdata['externals']['imdb']
seriesinfo['countryname'] = seriesdata['network']['country']['name']
seriesinfo['countrycode'] = seriesdata['network']['country']['code']
seriesinfo['timezone'] = seriesdata['network']['country']['timezone']
self.print_seriesdata(seriesinfo)
def print_seriesdata(self, seriesinfo):
print(f"Series ID: {seriesinfo['seriesid']}")
print(f"Series URL: {seriesinfo['seriesurl']}")
print(f"Series Name: {seriesinfo['seriesname']}")
print(f"Type of Series: {seriesinfo['seriestype']}")
print(f"Lanuguage: {seriesinfo['series_language']}")
print(f"Genres: {seriesinfo['series_genres']}")
print(f"Series Status: {seriesinfo['series_status']}")
print(f"Run Time: {seriesinfo['series_runtime']}")
print(f"Premier Date: {seriesinfo['series_premiered']}")
print(f"Official Website: {seriesinfo['offcialsite']}")
print(f"Day Aired: {seriesinfo['air_day']}")
print(f"Time Aired: {seriesinfo['air_time']}")
print(f"Rating: {seriesinfo['rating']}")
print(f"Weight: {seriesinfo['weight']}")
print(f"Network ID: {seriesinfo['networkid']}")
print(f"Network Name: {seriesinfo['networkname']}")
print(f"TVRage ID: {seriesinfo['tvrageid']}")
print(f"TVDB ID: {seriesinfo['tvdbid']}")
print(f"IMDB ID: {seriesinfo['imdbid']}")
print(f"Country: {seriesinfo['countryname']}")
print(f"Country Code: {seriesinfo['countrycode']}")
print(f"Timezone: {seriesinfo['timezone']}")
def process_episodedata(self, episodeinfo):
episodeinfo = {}
def print_episodedata(self, episodeinfo):
for episode in episodeinfo:
print(f"Air Date: {episode['airdate']}")
print(f"Air Time: {episode['airtime']}")
print(f"Season: {episode['season']}")
print(f"Episode: {episode['number']}")
print(f"Episode Name: {episode['name']}")
print(f"Summary: {episode['summary']}")
print(f"Rating: {episode['rating']['average']}")
print(f"Medium Image: {episode['image']['medium']}")
print(f"Original Image: {episode['image']['original']}")
def __del__(self):
pass
-287
View File
@@ -1,287 +0,0 @@
#!/usr/bin/env python3
from pymongo import MongoClient
import json
import psycopg2
import pandas as pd
from collections.abc import MutableMapping
from tqdm import tqdm
def flatten_dict(d: MutableMapping, sep: str = '.') -> MutableMapping:
[flat_dict] = pd.json_normalize(d, sep=sep).to_dict(orient='records')
return flat_dict
def delete_none(_dict):
"""
Delete None values recursively from all of the dictionaries, tuples,
lists, sets
"""
if isinstance(_dict, dict):
for key, value in list(_dict.items()):
if isinstance(value, (list, dict, tuple, set)):
_dict[key] = delete_none(value)
elif value is None or key is None:
del _dict[key]
elif isinstance(_dict, (list, set, tuple)):
_dict = type(_dict)(
delete_none(item) for item in _dict if item is not None
)
return _dict
def process_data(data, tablename):
inserts = []
failed_count = 0
seriesconflict = "ON CONFLICT (seriesid) DO UPDATE SET"
epconflict = "ON CONFLICT (episodeid, seriesid) DO UPDATE SET"
crewconflict = "ON CONFLICT (crewid) DO UPDATE SET"
actorconflict = "ON CONFLICT (actorid) DO UPDATE SET"
characterconflict = "ON CONFLICT (seriesid, characterid) DO UPDATE SET"
for document in data:
if document['name'].lower() == 'too many requests':
print("Skipping document due to 'Too Many Requests' in "
"document data")
continue
else:
new_document = delete_none(document)
if '_id' in new_document:
del new_document['_id']
try:
flatten_data = flatten_dict(new_document)
except TypeError as e:
print(
(
f"Unable to flatten data for current document. {e}\n"
"Exitting!"
)
)
failed_count += 1
else:
column_list = []
data_list = []
for key, value in flatten_data.items():
if isinstance(value, str):
value = value.replace('"', "'").replace("'", "''")
if key.lower() == '_links.self.href':
key = 'apilink'
if key.lower() == '_links.show.href':
key = 'apishowlink'
if key.lower() == '_links.show.name':
key = 'apishowname'
if '.' in key.lower():
key = key.replace('.', '')
if tablename == 'seriesdata' and (
key.lower() == 'name' or key.lower() == 'type'
):
key = f"series_{key}"
if tablename == 'seriesdata' and key.lower() == 'id':
key = "seriesid"
if tablename == 'crewdata' and key.lower() == 'id':
key = "crewid"
if (
tablename == 'actordata' and (
key.lower() == 'name' or key.lower() == 'number' or
key.lower() == 'type'
)
):
key = f"actor{key}"
if tablename == 'actordata' and key.lower() == 'id':
key = "actorid"
if tablename == 'characterdata' and key.lower() == 'id':
key = "characterid"
if tablename == 'characterdata' and key.lower() == 'name':
key = "charactername"
if tablename == 'characterdata' and key.lower() == 'id':
key = "seriesid, characterid"
if (
(
tablename == 'epdata' or
tablename == 'actordata'
) and (
key.lower() == 'name' or
key.lower() == 'number' or
key.lower() == 'type'
)
):
key = f"episode_{key}"
if key.lower() == 'language':
key = "language_name"
if tablename == 'epdata' and key.lower() == 'id':
key = "episodeid"
if tablename == 'actordata' and 'seriesid' in key.lower():
continue
else:
column_list.append(key)
if (
'genres' in key.lower() or
'schedule' in key.lower()
):
if isinstance(value, list):
value = ', '.join(value)
if isinstance(value, int) or isinstance(value, float):
value = str(json.dumps(value))
data_list.append(f"'{value}'")
columns = ", ".join(column_list)
column_data = ", ".join(data_list)
conflict_list = []
for indexer, column in enumerate(column_list):
conflict_list.append(
f"{column_list[indexer]} = {data_list[indexer]}"
)
conflict_data = ', '.join(conflict_list).replace("\\", "")
conflict_clause = ''
if tablename == 'seriesdata':
conflict_clause = seriesconflict
elif tablename == 'epdata':
conflict_clause = epconflict
elif tablename == 'crewdata':
conflict_clause = crewconflict
elif tablename == 'actordata':
conflict_clause = actorconflict
elif tablename == 'characterdata':
conflict_clause = characterconflict
insert_command = (
(
f"Insert Into updates.{tablename} ({columns}) "
f"values ({column_data}) "
f"{conflict_clause} {conflict_data};"
)
)
inserts.append(insert_command)
return inserts, failed_count
try:
connection_string = (
(
'postgres://postgres:Optimus0329@192.168.128.7:5432/media_dbsync'
'?options=-csearch_path%3Ddbo,public,updates'
)
)
pgclient = psycopg2.connect(connection_string)
pgcursor = pgclient.cursor()
# print(pgclient.get_dsn_parameters(), "\n")
print("Connected to postgresql database server.")
pgcursor.execute("SELECT version();")
record = pgcursor.fetchone()
print("You are connected to - ", record, "\n")
pgclient.set_isolation_level(0)
except Exception as e:
print(f"Unable to connect to postgresql database server:{e}\nExitting!")
exit()
host = '192.168.128.8'
port = 27017
print(f"Connecting to mongodb at {host}:{port}")
try:
connection_string = (
f"mongodb://{host}:{port}"
)
mgclient = MongoClient(connection_string)
mgdb = mgclient['test2']
except BaseException as e:
print(f"Unable to connect to mongodb.{e}\nExitting!")
exit()
else:
mgseries = mgdb["series"]
mgseries.create_index('id', unique=True)
mgepisodes = mgdb["episodes"]
mgepisodes.create_index('id', unique=True)
mgactors = mgdb["actors"]
mgactors.create_index('id', unique=True)
mgcharacters = mgdb["characters"]
mgcharacters.create_index('id', unique=True)
mgcrew = mgdb["crew"]
mgcrew.create_index('id', unique=True)
print(
f"Connection to mongodb at {host}:"
f"{port} successful."
)
print("Creating Insert commands from collection data.")
print("Collecting series data for series inserts.")
series_list = mgseries.find({})
inserts, failed_count = process_data(series_list, 'seriesdata')
print(f"Total number of series inserts: {len(inserts)}")
for i in tqdm(range(len(inserts))):
try:
insert = inserts[i]
pgcursor.execute(insert)
except Exception as e:
print(f"Unable to insert data into seriesdata table. {e}")
failed_count += 1
print(f"Total number of failed series inserts: {failed_count}")
pgclient.commit()
epinserts = []
failures = 0
episode_array = mgepisodes.find({})
print("Collecting episode data for episode inserts.")
inserts, failed_count = process_data(episode_array, 'epdata')
print(f"Total number of episode inserts: {len(inserts)}")
for i in tqdm(range(len(inserts))):
try:
insert = inserts[i]
pgcursor.execute(insert)
except Exception as e:
print(f"Unable to insert data into epdata table. {e}")
failed_count += 1
print(f"Total number of failed episode inserts: {failed_count}")
pgclient.commit()
failures = 0
actor_array = mgactors.find({})
print("Collecting actor data for actor inserts.")
inserts, failed_count = process_data(actor_array, 'actordata')
print(f"Total number of actor inserts: {len(inserts)}")
for i in tqdm(range(len(inserts))):
try:
insert = inserts[i]
pgcursor.execute(insert)
except Exception as e:
print(f"Unable to insert data into actors table. {e}")
failed_count += 1
print(f"Total number of failed actor inserts: {failed_count}")
pgclient.commit()
failures = 0
character_array = mgcharacters.find({})
print("Collecting actor data for actor inserts.")
inserts, failed_count = process_data(character_array, 'characterdata')
print(f"Total number of character inserts: {len(inserts)}")
for i in tqdm(range(len(inserts))):
try:
insert = inserts[i]
pgcursor.execute(insert)
except Exception as e:
print(f"Unable to insert data into character table. {e}")
failed_count += 1
print(f"Total number of failed character inserts: {failed_count}")
pgclient.commit()
failures = 0
crew_array = mgcrew.find({})
print("Collecting crew data for crew inserts.")
inserts, failed_count = process_data(crew_array, 'crewdata')
print(f"Total number of crew inserts: {len(inserts)}")
for i in tqdm(range(len(inserts))):
try:
insert = inserts[i]
pgcursor.execute(insert)
except Exception as e:
print(f"Unable to insert data into epdata table. {e}")
failed_count += 1
print(f"Total number of failed crew inserts: {failed_count}")
pgclient.commit()
pgclient.close()
print("PostgreSQL connection is closed")
exit()
+57 -84
View File
@@ -1,95 +1,68 @@
#!/usr/bin/env python3 #!/usr/bin/python3
import os import os
import json import json
import requests import requests
import time
import logging
def download_json_to_file(api_url, outputdir, output_file, lprint):
def load(self, rawdata): """
jdata = {} Downloads JSON data from the given API URL and saves it to the specified file.
jdata = json.loads(rawdata) """
newdata = self.dump(jdata)
return (newdata)
def dump(self, rawdata):
jdata = json.dumps(rawdata, indent=4)
return (jdata)
def jconvert(self, inputdata):
outputdata = json.loads(inputdata)
return (outputdata)
def populate_cache(self, jdata, jfile):
try: try:
with open(jfile, 'w') as datafile: response = requests.get(api_url)
datafile.write(jdata) response.raise_for_status() # Raise an error for bad status codes
except Exception: data = response.json()
print('Unable to save cache file.') with open(f"{outputdir}{output_file}", 'w') as f:
else:
datafile.close()
def fetch_json(weburl, seriesid, cachefile):
try:
webdata = get_data(weburl)
jdata = json.load(webdata)
except Exception as e:
print('Unable to get JSON data for ' + str(seriesid) + ': ' + str(e))
else:
if jdata == '[]':
pass
else:
populate_cache(jdata, cachefile)
def get_data(weburl):
try:
showresponse = requests.get(weburl)
except requests.exceptions.ConnectionError:
print('Unable to connect and get JSON data for url ' + str(weburl))
else:
return (showresponse.text)
def check_and_download(URL, series_id, cachefile):
try:
mazeurl = URL.replace('<seriesid>', str(series_id))
response = requests.get(mazeurl)
data = json.loads(response.text)
except FileNotFoundError:
# print(f'{cachefile} does not exist. Attempting to download.')
jget.fetch_json(
URL.replace('SERIESID', str(series_id)),
series_id,
cachefile
)
except Exception as e:
print(f'Unable to download {cachefile}. {e}')
else:
with open(cachefile, 'w') as f:
json.dump(data, f, indent=4) json.dump(data, f, indent=4)
f.close() # 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}")
new_updates = [] def main():
listing = open('D:/Temp/filelist.txt', 'w') # 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'
os.chdir('D:/Projects/new_dbsync/cache/series') lprint = Logprint('output')
# replace with your directory path try:
directory_path = 'D:/Projects/new_dbsync/cache/series' download_json_to_file(api_url, outputdir, outputfile)
files = [ print("Download successful!")
f for f in os.listdir(directory_path) except Exception as e:
if os.path.isfile(os.path.join(directory_path, f)) # Handle any exceptions that occur during the download process and log them
] lprint.logprint("info", f"Downloading {outputdir}{outputfile} failed with error:{e}")
for filename in files: exit(1)
with open(filename, 'r') as file:
content = file.read()
if 'Not Found' in content or 'Too Many Requests' in content:
seriesid = filename.split('.')[0]
new_updates.append(seriesid)
listing.write(seriesid + '\n')
listing.close()
exit() 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()
+19 -2
View File
@@ -44,12 +44,12 @@ class LPrint:
"""Check the log file size and rotate if necessary.""" """Check the log file size and rotate if necessary."""
log_size = os.path.getsize(self.logfile) log_size = os.path.getsize(self.logfile)
print(f'Logfile Size: {log_size} ({self.humanbytes(log_size)})') 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() self.rotate_logs()
def rotate_logs(self): def rotate_logs(self):
"""Rotate the log file.""" """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) rotater = LogRotate(prefix=self.logfile, verbose=True)
try: try:
rotater.rotate() rotater.rotate()
@@ -79,6 +79,23 @@ class LPrint:
else: else:
logging.error(f'Unknown log level: {loglevel}. Message: {logmessage}') logging.error(f'Unknown log level: {loglevel}. Message: {logmessage}')
def screenprint(self, loglevel, logmessage):
"""Log a message with a timestamp."""
logstamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_message = f'{logstamp}: {logmessage}'
loglevel = loglevel.lower()
if loglevel == 'debug':
logging.debug(log_message)
print(f"{log_message}")
elif loglevel == 'warning':
logging.warning(log_message)
print(f"{log_message}")
elif loglevel == 'info':
logging.info(log_message)
print(f"{log_message}")
else:
logging.error(f'Unknown log level: {loglevel}. Message: {logmessage}')
def create_rotating_log(self, logpath, max_bytes=10_000_000, backup_count=5): def create_rotating_log(self, logpath, max_bytes=10_000_000, backup_count=5):
"""Create a rotating log handler.""" """Create a rotating log handler."""
logger = logging.getLogger("Rotating Log") logger = logging.getLogger("Rotating Log")
+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)
+394
View File
@@ -0,0 +1,394 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import logging
import sys
from typing import Dict, Tuple, Any
from db.functions import settype, dbmongo
from db.schema_generator import MongoToPostgresSchemaGenerator, MongoDocumentInserter
import settings.config
# Configuration constants
SAMPLE_SIZE = 10000
BATCH_SIZE = 10000
SCHEMA_NAME = 'updates'
PK_FIELD = 'id'
# Table-to-collection mapping
TABLE_CONFIG = {
'seriesdata': 'mgseries',
'episodesdata': 'mgepisodes',
'actorsdata': 'mgactors',
'charactersdata': 'mgcharacters',
'crewdata': 'mgcrew',
}
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
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
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())
+2 -2
View File
@@ -1,11 +1,11 @@
logrotater==1.3 logrotater==1.3
numpy==2.2.3 numpy==2.2.3
progressbar33==2.4 progressbar33==2.4
psycopg2==2.9.10 psycopg2-binary==2.9.10
pymongo==4.11.2 pymongo==4.11.2
pymssql==2.3.2 pymssql==2.3.2
Requests==2.32.3 Requests==2.32.3
SQLAlchemy==2.0.39 SQLAlchemy==2.0.39
pymysql pymysql
tqdm tqdm
pandas pandas
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd /srv/Projects/new_dbsync
/srv/Projects/new_dbsync/update_json.py >> /home/wjones/update_mongo.log
+55
View File
@@ -61,3 +61,58 @@ class Config(object):
def __del__(self): def __del__(self):
pass pass
def read_config_file(path_or_rootdir):
"""Read configuration and return a dict of options.
If `path_or_rootdir` is a file path to a config file, it will be used
directly. Otherwise it's treated as the project root directory and
the config file is expected at `<ROOTDIR>/settings/tvsync_settings.cfg`.
"""
configurer = configparser.ConfigParser()
# determine config file path
if os.path.isfile(path_or_rootdir):
cfg_path = path_or_rootdir
else:
cfg_path = os.path.join(path_or_rootdir, 'settings', 'tvsync_settings.cfg')
configurer.read(cfg_path)
options = {}
# Mirror keys used by the Config class
options['loglevel'] = configurer.get('global', 'loglevel')
options['hostname'] = configurer.get('dbsettings', 'hostname')
options['mghostname'] = configurer.get('dbsettings', 'mghostname')
options['dbname'] = configurer.get('dbsettings', 'dbname')
options['mgdbname'] = configurer.get('dbsettings', 'mgdbname')
options['mysqlusername'] = configurer.get('dbsettings', 'mysqlusername')
options['pgsqlusername'] = configurer.get('dbsettings', 'pgsqlusername')
options['mssqlusername'] = configurer.get('dbsettings', 'mssqlusername')
options['mysqlpassword'] = configurer.get('dbsettings', 'mysqlpassword')
options['pgsqlpassword'] = configurer.get('dbsettings', 'pgsqlpassword')
options['mssqlpassword'] = configurer.get('dbsettings', 'mssqlpassword')
options['pgsqlport'] = configurer.get('dbsettings', 'pgsqlport')
options['mysqlport'] = configurer.get('dbsettings', 'mysqlport')
options['mssqlport'] = configurer.get('dbsettings', 'mssqlport')
options['mgport'] = configurer.get('dbsettings', 'mgport')
options['updatetype'] = configurer.get('updatetype', 'type')
options['apitype'] = configurer.get('api', 'type')
options['updateschema'] = configurer.get('database', 'updateschema')
options['dbtype'] = configurer.get('database', 'dbtype')
options['initdb'] = configurer.get('database', 'initialize')
options['initload'] = configurer.get('database', 'initload')
options['skipcache'] = configurer.get('updatetype', 'cacheskip')
options['refreshcache'] = configurer.get('updatetype', 'refreshcache')
options['full'] = configurer.get('updatetype', 'full')
options['monthly'] = configurer.get('updatetype', 'monthly')
options['weekly'] = configurer.get('updatetype', 'weekly')
options['daily'] = configurer.get('updatetype', 'daily')
options['12h'] = configurer.get('updatetype', '12h')
options['6h'] = configurer.get('updatetype', '6h')
options['custom'] = configurer.get('updatetype', 'custom')
options['serieslist'] = list()
options['timedifference'] = configurer.get('updatetype', options['updatetype'])
options['current_timestamp'] = time.time()
if isinstance(options['timedifference'], str) and options['timedifference'].lower() == 'full':
options['timedifference'] = int(options['current_timestamp'])
return options
+4 -4
View File
@@ -1,12 +1,12 @@
[global] [global]
loglevel=debug loglevel=info
[dbsettings] [dbsettings]
sqlserver_driver=ODBC Driver 13 for SQL Server sqlserver_driver=ODBC Driver 13 for SQL Server
hostname=192.168.128.7 hostname=192.168.128.3
mghostname=192.168.128.8 mghostname=192.168.128.24
dbname=media_dbsync dbname=media_dbsync
mgdbname=test2 mgdbname=tvdata
mysqlusername=root mysqlusername=root
pgsqlusername=postgres pgsqlusername=postgres
mssqlusername=sa mssqlusername=sa
+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
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
+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
File diff suppressed because it is too large Load Diff
Executable
+340
View File
@@ -0,0 +1,340 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import sys
import url.urls
import url.process
from tqdm import tqdm
import db.sql
import logs.Logger
from db.functions import settype, dbmongo
from datetime import datetime
import json
import json_downloader
response = None
def set_apienv(dbengine, dbExec, 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
def print_starttime(lprint, ts1):
lprint.logprint("info", f"Sync start time: {str(ts1)}")
def get_tvdb(tvdburls):
return tvdburls.get_data()
def get_tvmaze(tvmazeurls):
return tvmazeurls.get_data()
def find_file_in_multiple_dirs(filename, directories):
"""
Checks if a file exists in any of the provided directories.
Args:
filename (str): The name of the file to search for.
directories (list): A list of directory paths to check.
Returns:
Path or None: The full path to the file if found, otherwise None.
"""
directories_found = []
for directory in directories:
if os.path.isfile(f'{directory}{filename}'):
directories_found.append(directory)
return directories_found
def main() -> int:
ROOTDIR = os.getcwd()
directories = {}
# Use cross-platform path builders and ensure directories exist
LOGDIR = os.path.join(ROOTDIR, "log")
directories["SERIESDIR"] = os.path.join(ROOTDIR, "cache", "series") + os.sep
directories["EPISODEDIR"] = os.path.join(ROOTDIR, "cache", "episodes") + os.sep
directories["CASTDIR"] = os.path.join(ROOTDIR, "cache", "cast") + os.sep
directories["GUESTCASTDIR"] = os.path.join(ROOTDIR, "cache", "guestcast") + os.sep
directories["CREWDIR"] = os.path.join(ROOTDIR, "cache", "crew") + os.sep
#directories["GUESTCREWDIR"] = os.path.join(ROOTDIR, "cache", "guestcrew") + os.sep
#directories["CREDITSDIR"] = os.path.join(ROOTDIR, "cache", "credits") + os.sep
#directories["ALIASDIR"] = os.path.join(ROOTDIR, "cache", "aliases") + os.sep
tempdir = os.path.join(ROOTDIR, 'temp') + os.sep
skip_file = f"{tempdir}skip_ids.txt"
# Create directories if missing so later code doesn't fail
try:
os.makedirs(LOGDIR, exist_ok=True)
os.makedirs(os.path.join(ROOTDIR, "cache", "series"), exist_ok=True)
os.makedirs(os.path.join(ROOTDIR, "cache", "episodes"), exist_ok=True)
os.makedirs(os.path.join(ROOTDIR, "cache", "cast"), exist_ok=True)
os.makedirs(os.path.join(ROOTDIR, "cache", "guestcast"), exist_ok=True)
os.makedirs(os.path.join(ROOTDIR, "cache", "crew"), exist_ok=True)
os.makedirs(os.path.join(ROOTDIR, "cache", "guestcrew"), exist_ok=True)
except Exception:
# If directory creation fails, let the logging initialization surface the error
pass
lprint = logs.Logger.LPrint(LOGDIR, False)
lprint.start_log()
import settings.config
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
uprocess = url.process.Loadurl()
TVMAZEURLS = url.urls.Tvmazeurls()
TVMAZE_URLS = get_tvmaze(TVMAZEURLS)
TVDBURLS = url.urls.TVDBurls()
TVDB_URLS = get_tvdb(TVDBURLS)
updateRange = None
lprint.logprint("info", "Creating JSON instance for data retrieval.")
lprint.logprint("info", "JSON instance created.")
lprint.logprint("info", f"Update type is {options['updatetype']}")
timedifference = options["updatetype"]
currenttimestamp = time.time()
if timedifference.lower() == "full":
timedifference = int(currenttimestamp)
basetime = str(currenttimestamp).split(".")
currenttimestamp = int(basetime[0])
ts1 = str(datetime.now()).split(".")[0]
lprint.logprint("info", "Sync start time: " + str(ts1))
print_starttime(lprint, ts1)
dbExec = db.sql.Dbexec()
createTempTables = dbclass["CreateTemptables"]
new_updates = {}
ct = createTempTables(
dbengine["session"],
dbengine["metadata"],
dbengine["engine"]
)
tempTableList = ct.tablelist
new_updates = []
if options["apitype"].lower() == "tvmaze":
try:
new_updates = set_apienv(
dbengine,
dbExec,
lprint
)
except Exception as exc: # noqa: PLC0301 - broad handler to log failures
lprint.logprint("error", f"Failed to set API env: {exc}")
return 1
timeDifference = options["updatetype"]
currentTimestamp = time.time()
if timeDifference.lower() == "full" or options["initload"] == "1":
tdifference = 0
else:
tdifference = int(currentTimestamp) - int(
options[options["updatetype"]]
)
needed_updates = []
updateList = []
for result in new_updates:
updateid = result[0]
updatestamp = int(result[1])
tdif = int(tdifference)
if updatestamp > tdif:
needed_updates.append(updateid)
updateList.append(updateid)
lprint.logprint("info", "Update Type: " + str(options["updatetype"]))
updateRange = len(updateList)
lprint.logprint("info", "Number of updates: " + str(updateRange))
# Save a downloaded copy of the updates JSON for debugging/inspection
try:
os.makedirs(tempdir, exist_ok=True)
out_file = 'tvmaze_updates.json'
prev_file = f'post_tvmaze_updates.json.{currentTimestamp}'
previous_updates_file = f"{tempdir}{prev_file}"
# TVMAZE_URLS is a dict of urls; use the updatesurl if available
updates_url = TVMAZE_URLS.get('updatesurl') if isinstance(TVMAZE_URLS, dict) else None
if updates_url:
json_downloader.download_json_to_file(updates_url, tempdir, out_file, lprint)
# load and convert saved JSON into a dict (id -> timestamp)
try:
downloaded_updates_dict = {}
downloaded_updates_list = []
if os.path.exists(f"{tempdir}{out_file}"):
with open(f"{tempdir}{out_file}", 'r') as jf:
loaded = json.load(jf)
if isinstance(loaded, dict):
for k, v in loaded.items():
try:
kid = int(k)
except Exception:
kid = k
try:
vt = int(v)
except Exception:
vt = v
downloaded_updates_dict[kid] = vt
downloaded_updates_list = list(downloaded_updates_dict.items())
elif isinstance(loaded, list):
for item in loaded:
if isinstance(item, list) and len(item) >= 2:
k = item[0]; v = item[1]
elif isinstance(item, dict):
if 'id' in item and 'timestamp' in item:
k = item['id']; v = item['timestamp']
elif 'seriesid' in item and 'timestamp' in item:
k = item['seriesid']; v = item['timestamp']
else:
downloaded_updates_list.append(item)
continue
else:
k = item; v = None
try:
kid = int(k)
except Exception:
kid = k
try:
vt = int(v) if v is not None and str(v).isdigit() else v
except Exception:
vt = v
downloaded_updates_dict[kid] = vt
downloaded_updates_list.append((kid, vt))
else:
downloaded_updates_list = [loaded]
lprint.logprint("info", f"Loaded {len(downloaded_updates_list)} entries from tvmaze_updates.json")
else:
downloaded_updates_dict = {}
downloaded_updates_list = []
except Exception:
downloaded_updates_dict = {}
downloaded_updates_list = []
except Exception:
# non-fatal; continue
lprint.logprint("warning", "Failed to save or load downloaded updates JSON.")
previouslisting = dbExec.rawsql_select(
dbengine["engine"],
"select seriesid, timestamp from updates.tvupdates order by seriesid",
lprint
)
print(f"Saving previous updates to file.")
with open(f"{previous_updates_file}", 'w') as pjf:
for pl in previouslisting:
pjf.write(f"{pl[0]}: {pl[1]}\n")
pjf.close()
# convert previouslisting (list of tuples) into a dict: row_id -> country_name
try:
_previouslisting_list = previouslisting
previous_listing_dict = {row[0]: row[1] for row in _previouslisting_list}
except Exception:
# leave previouslisting as-is on any failure
pass
# Ensure dictionaries exist
try:
downloaded_updates_dict
except NameError:
downloaded_updates_dict = {}
try:
previous_listing_dict
except NameError:
previous_listing_dict = {}
files_needed = []
directory_list = []
for value in directories.values():
directory_list.append(value)
skip_ids = {}
skipfile = open(skip_file, 'r')
for line in skipfile:
field = line.split(';')
skip_ids[field[0]] = field[1]
print(f"Updating update table.")
dbExec.update_tvupdates(
dbengine["engine"],
downloaded_updates_dict,
"updates.tvupdates"
)
print("update complete.")
print(f"Processing {len(downloaded_updates_dict)} downloaded updates against {len(previous_listing_dict)} previous updates for files needed.")
for seriesid, ts in downloaded_updates_dict.items():
if seriesid in previous_listing_dict:
prev_ts = previous_listing_dict[seriesid]
# Check to see if all json files are present for seriesid
try:
if int(ts) == int(prev_ts):
found_path = find_file_in_multiple_dirs(f"{seriesid}.json", directory_list)
if len(found_path) == 5:
continue
else:
for directory in directory_list:
if directory not in found_path:
files_needed.append([f"{directory}", f"{seriesid}.json"])
elif int(ts) > int(prev_ts):
for directory in directory_list:
files_needed.append([f"{directory}", f"{seriesid}.json"])
except Exception:
lprint.logprint("debug", f"Unable to process Series {seriesid} timestamps: downloaded {ts} vs previous {prev_ts}")
else:
# Add needed json files to needed file list.
for directory in directory_list:
files_needed.append([f"{directory}", f"{seriesid}.json"])
lprint.logprint("debug", f"Series {seriesid} present in downloaded updates but missing in previous listing")
lprint.logprint("info", f"Downloadinng JSON for {len(files_needed)} series IDs.")
print(f"Downloading JSON for {len(files_needed)} files.")
for i in tqdm(range(len(files_needed)), desc="Downloading JSON files", unit="filename"):
for directory, file in files_needed[i:i+1]:
if 'credits' in directory:
if file.replace('.json', '') in skip_ids and 'castcredits' in skip_ids[file.replace('.json', '')].strip():
lprint.logprint("info", f"Skipping credits download for series ID {file.replace('.json', '')} as per skip_ids.txt")
continue
else:
url_template = TVMAZE_URLS["creditsurl"]
elif 'crew' in directory:
url_template = TVMAZE_URLS["crewurl"]
elif 'cast' in directory:
url_template = TVMAZE_URLS["casturl"]
elif 'episodes' in directory:
url_template = TVMAZE_URLS["episodesurl"]
elif 'aliases' in directory:
url_template = TVMAZE_URLS["aliasurl"]
else:
url_template = TVMAZE_URLS["showurl"]
seriesid = file.replace('.json', '')
json_downloader.download_json_to_file(url_template.replace("<seriesid>", str(seriesid)), directory, f'{seriesid}.json', lprint)
lprint.logprint("info", "JSON download complete.")
if __name__ == "__main__":
sys.exit(main())
-170
View File
@@ -1,170 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import url.urls
import url.process
import api.tvmaze.cast as mazecast
import api.tvmaze.crew as mazecrew
import api.tvmaze.episodes as mazeepisodes
import api.tvmaze.series as mazeseries
import db.sql
import jprocessor.process
import logs.Logger
from db.functions import settype, dbmongo
from datetime import datetime
response = None
def set_apienv(urls, dbExec, updatesBase):
updatetable = updatesBase[8]
inputdata = uprocess.get_data(urls["updatesurl"])
availableupdates = jget.jconvert(inputdata)
print(f"Retrieved {len(availableupdates)} rows for processing......")
dbExec.update_tvupdates(dbengine["engine"], availableupdates, updatetable)
newupdates = dbExec.rawsql_select(
dbengine["engine"],
"select seriesid,timestamp from updates.tvupdates",
lprint
)
return newupdates
def print_starttime(lprint, ts1):
lprint.logprint("info", f"Sync start time: {str(ts1)}")
def get_tvdb():
tvdb_urls = TVDBURLS.get_data()
return tvdb_urls
def get_tvmaze():
tvmaze_urls = TVMAZEURLS.get_data()
return tvmaze_urls
if __name__ == "__main__":
ROOTDIR = os.getcwd()
directories = {}
if os.name == "nt":
LOGDIR = f"{ROOTDIR}\\log"
directories["SERIESDIR"] = f"{ROOTDIR}\\cache\\series\\"
directories["EPISODEDIR"] = f"{ROOTDIR}\\cache\\episodes\\"
directories["CASTDIR"] = f"{ROOTDIR}\\cache\\cast\\"
directories["CREWDIR"] = f"{ROOTDIR}\\cache\\crew\\"
else:
LOGDIR = f"{ROOTDIR}/log"
directories["SERIESDIR"] = f"{ROOTDIR}/cache/series/"
directories["EPISODEDIR"] = f"{ROOTDIR}/cache/episodes/"
directories["CASTDIR"] = f"{ROOTDIR}/cache/cast/"
directories["CREWDIR"] = f"{ROOTDIR}/cache/crew/"
lprint = logs.Logger.LPrint(LOGDIR, False)
lprint.start_log()
import settings.config
config_options = settings.config.Config(ROOTDIR)
options = config_options.config_options
dbtype, apitype = options["dbtype"], options["apitype"]
dbs = settype(dbtype, apitype, options)
dbclass, dbengine, apiengine = dbs.dbclass, dbs.dbengine, dbs.apiengine
uprocess = url.process.Loadurl()
TVMAZEURLS = url.urls.Tvmazeurls()
TVMAZE_URLS = get_tvmaze()
TVDBURLS = url.urls.TVDBurls()
TVDB_URLS = get_tvdb()
mseries = mazeseries.tvshow()
mcast = mazecast.cast()
mepisode = mazeepisodes.episodes()
mcrew = mazecrew.crew()
countrydictionary = {}
updateRange = None
lprint.logprint("info", "Creating JSON instance for data retrieval.")
jget = jprocessor.process.Jsonload()
lprint.logprint("info", "JSON instance created.")
print(f"Update type is {options['updatetype']}")
timedifference = options["updatetype"]
currenttimestamp = time.time()
if timedifference.lower() == "full":
timedifference = int(currenttimestamp)
basetime = str(currenttimestamp).split(".")
currenttimestamp = int(basetime[0])
ts1 = str(datetime.now()).split(".")[0]
print("Sync start time: " + str(ts1))
print_starttime(lprint, ts1)
updatesBase = dbengine["updatesBase"]
dboBase = dbengine["dboBase"]
session = dbengine["session"]
dbExec = db.sql.Dbexec()
createTables = dbclass["Createtables"]
Tables = dbclass["Tables"]
createTempTables = dbclass["CreateTemptables"]
new_updates = {}
sqlexec = db.sql.SQLGenerate()
mt = createTables(
dbengine["session"],
dbengine["metadata"],
dbengine["engine"]
)
ct = createTempTables(
dbengine["session"],
dbengine["metadata"],
dbengine["engine"]
)
tempTableList = ct.tablelist
if options["apitype"].lower() == "tvmaze":
new_updates = set_apienv(TVMAZE_URLS, dbExec, tempTableList)
from cache.process import Cacher
cacher = Cacher()
cacher.process(options, jget, new_updates, ROOTDIR, TVMAZE_URLS)
timeDifference = options["updatetype"]
currentTimestamp = time.time()
baseTime = str(currentTimestamp).split(".")
if timeDifference.lower() == "full" or options["initload"] == "1":
tdifference = 0
else:
tdifference = int(currentTimestamp) - int(
options[options["updatetype"]]
)
needed_updates = []
updateList = []
for result in new_updates:
updateid = result[0]
updatestamp = int(result[1])
tdif = int(tdifference)
if updatestamp > tdif:
needed_updates.append(updateid)
updateList.append(updateid)
print("Update Type: " + str(options["updatetype"]))
updateRange = len(updateList)
print("Number of updates: " + str(updateRange))
cacher.update_cache(
jget, updateList, ROOTDIR, TVMAZE_URLS, options, lprint
)
countrylisting = dbExec.rawsql_select(
dbengine["engine"],
"select row_id, country_name from dbo.countrydata",
lprint
)
for country in countrylisting:
country_name = country[1]
country_id = country[0]
countrydictionary[country_name] = country_id
mongo_updater = dbmongo(options)
mongo_updater.update_mongo(
updateList,
ROOTDIR,
directories,
jget,
countrydictionary,
dbExec,
dbengine,
dboBase,
lprint,
)
exit(0)
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import sys
from tqdm import tqdm
import logs.Logger
from db.functions import settype, dbmongo
from datetime import datetime
import json
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
def print_starttime(lprint, ts1):
lprint.logprint("info", f"Sync start time: {str(ts1)}")
def find_file_in_multiple_dirs(filename, directories):
"""
Checks if a file exists in any of the provided directories.
Args:
filename (str): The name of the file to search for.
directories (list): A list of directory paths to check.
Returns:
Path or None: The full path to the file if found, otherwise None.
"""
directories_found = []
for directory in directories:
if os.path.isfile(f'{directory}{filename}'):
directories_found.append(directory)
return directories_found
def load_json_to_mongodb(mongo_updater, directories, update_list, updateDict, lprint, track_changes=True):
"""
Load downloaded JSON files into MongoDB collections with change detection.
Processes series, episodes, cast, and crew data from cached JSON files
and inserts or updates them in the appropriate MongoDB collections.
Automatically detects if documents are new, updated, or unchanged by comparing
updateDict timestamps with existing document 'updated' fields.
Args:
mongo_updater: dbmongo instance for MongoDB operations
directories: dict containing cache directory paths for different data types
update_list: list of series IDs to process
updateDict: dict with seriesid as key and timestamp as value
lprint: logger instance for logging operations
track_changes: bool, if True tracks inserted vs updated documents (default: True)
Returns:
tuple: (successful_count, failed_count, stats_dict) for series processed
stats_dict contains: {'inserted': count, 'updated': count}
"""
successful_count = 0
failed_count = 0
stats = {'series_inserted': 0, 'series_updated': 0, 'series_skipped': 0,
'episodes_inserted': 0, 'episodes_updated': 0, 'episodes_skipped': 0,
'cast_inserted': 0, 'cast_updated': 0, 'cast_skipped': 0,
'crew_inserted': 0, 'crew_updated': 0, 'crew_skipped': 0}
lprint.logprint("info", f"Starting to load {len(update_list)} series into MongoDB")
for seriesid in tqdm(update_list[1:-1], desc="Loading series data into MongoDB", unit="series"):
seriesid_str = int(seriesid)
try:
# Load series data
series_file = f"{directories['SERIESDIR']}{seriesid}.json"
if os.path.exists(series_file):
try:
with open(series_file, 'r') as f:
series_data = json.load(f)
# Insert or update series in MongoDB
if series_data:
if track_changes:
existing = mongo_updater.mgseries.find_one({"id": series_data.get("id")})
#seriesid_str = str(seriesid)
update_timestamp = updateDict.get(seriesid_str)
if existing:
# Check if data has changed by comparing update timestamp
if update_timestamp and int(update_timestamp) > existing.get("updated", 0):
mongo_updater.bulk_upsert_by_id(mongo_updater.mgseries, [series_data])
stats['series_updated'] += 1
lprint.logprint("debug", f"Updated series data for ID {seriesid}")
else:
lprint.logprint("debug", f"Series {seriesid} unchanged, skipping")
stats['series_skipped'] += 1
pass # Document unchanged
else:
mongo_updater.bulk_upsert_by_id(mongo_updater.mgseries, [series_data])
stats['series_inserted'] += 1
lprint.logprint("debug", f"Inserted new series data for ID {seriesid}")
# Load episodes data
episodes_file = f"{directories['EPISODEDIR']}{seriesid}.json"
if os.path.exists(episodes_file):
try:
with open(episodes_file, 'r') as f:
episodes_data = json.load(f)
if isinstance(episodes_data, list) and episodes_data:
# Add seriesid to each episode
for episode in episodes_data:
episode['seriesid'] = seriesid_str
if track_changes:
for episode in episodes_data:
existing = mongo_updater.mgepisodes.find_one({"id": episode.get("id")})
#seriesid_str = str(seriesid)
update_timestamp = updateDict.get(seriesid_str)
if existing:
if update_timestamp and int(update_timestamp) > existing.get("updated", 0):
mongo_updater.bulk_upsert_by_id(mongo_updater.mgepisodes, episodes_data)
stats['episodes_updated'] += 1
else:
lprint.logprint("debug", f"Episode Data for Series {seriesid} unchanged, skipping")
pass # Document unchanged
else:
mongo_updater.bulk_upsert_by_id(mongo_updater.mgepisodes, episodes_data)
stats['episodes_inserted'] += 1
#mongo_updater.bulk_upsert_by_id(mongo_updater.mgepisodes, episodes_data)
lprint.logprint("debug", f"Loaded {len(episodes_data)} episodes for series {seriesid}")
except Exception as e:
lprint.logprint("warning", f"Failed to load episodes file for {seriesid}: {e}")
# Load cast data
cast_file = f"{directories['CASTDIR']}{seriesid}.json"
if os.path.exists(cast_file):
try:
with open(cast_file, 'r') as f:
cast_data = json.load(f)
if isinstance(cast_data, list) and cast_data:
actors = []
characters = []
for item in cast_data:
if 'person' in item and item['person']:
item['person']['seriesid'] = seriesid_str
actors.append(item['person'])
if 'character' in item and item['character']:
item['character']['seriesid'] = seriesid_str
characters.append(item['character'])
if track_changes:
for actor in actors:
existing = mongo_updater.mgactors.find_one({"id": actor.get("id")})
#seriesid_str = str(seriesid)
update_timestamp = updateDict.get(seriesid_str)
if existing:
if update_timestamp and int(update_timestamp) > existing.get("updated", 0):
stats['cast_updated'] += 1
else:
stats['cast_inserted'] += 1
if actors:
mongo_updater.bulk_upsert_by_id(mongo_updater.mgactors, actors)
if characters:
mongo_updater.bulk_upsert_by_id(mongo_updater.mgcharacters, characters)
lprint.logprint("debug", f"Loaded cast data for series {seriesid}")
except Exception as e:
lprint.logprint("warning", f"Failed to load cast file for {seriesid}: {e}")
# Load crew data
crew_file = f"{directories['CREWDIR']}{seriesid}.json"
if os.path.exists(crew_file):
try:
with open(crew_file, 'r') as f:
crew_data = json.load(f)
if isinstance(crew_data, list) and crew_data:
crew_list = []
for item in crew_data:
if 'person' in item and item['person']:
item['person']['seriesid'] = seriesid_str
item['person']['crew_type'] = item.get('type', 'unknown')
crew_list.append(item['person'])
if track_changes:
for crew in crew_list:
existing = mongo_updater.mgcrew.find_one({"id": crew.get("id")})
#seriesid_str = str(seriesid)
update_timestamp = updateDict.get(seriesid_str)
if existing:
if update_timestamp and int(update_timestamp) > existing.get("updated", 0):
stats['crew_updated'] += 1
else:
stats['crew_inserted'] += 1
if crew_list:
mongo_updater.bulk_upsert_by_id(mongo_updater.mgcrew, crew_list)
lprint.logprint("debug", f"Loaded crew data for series {seriesid}")
except Exception as e:
lprint.logprint("warning", f"Failed to load crew file for {seriesid}: {e}")
successful_count += 1
lprint.logprint("info", f"Successfully processed series {seriesid}")
#mongo_updater.bulk_upsert_by_id(mongo_updater.mgseries, [series_data])
except Exception as e:
lprint.logprint("warning", f"Failed to load series file for {seriesid}: {e}")
failed_count += 1
continue
except Exception as e:
failed_count += 1
lprint.logprint("error", f"Unexpected error processing series {seriesid}: {e}")
lprint.logprint("info", f"MongoDB loading complete.")
return successful_count, failed_count, stats
def main() -> int:
ROOTDIR = os.getcwd()
directories = {}
# Use cross-platform path builders and ensure directories exist
LOGDIR = os.path.join(ROOTDIR, "log")
directories["SERIESDIR"] = os.path.join(ROOTDIR, "cache", "series") + os.sep
directories["EPISODEDIR"] = os.path.join(ROOTDIR, "cache", "episodes") + os.sep
directories["CASTDIR"] = os.path.join(ROOTDIR, "cache", "cast") + os.sep
directories["GUESTCASTDIR"] = os.path.join(ROOTDIR, "cache", "guestcast") + os.sep
directories["CREWDIR"] = os.path.join(ROOTDIR, "cache", "crew") + os.sep
#directories["GUESTCREWDIR"] = os.path.join(ROOTDIR, "cache", "guestcrew") + os.sep
#directories["CREDITSDIR"] = os.path.join(ROOTDIR, "cache", "credits") + os.sep
#directories["ALIASDIR"] = os.path.join(ROOTDIR, "cache", "aliases") + os.sep
tempdir = os.path.join(ROOTDIR, 'temp') + os.sep
lprint = logs.Logger.LPrint(LOGDIR, False)
lprint.start_log()
import settings.config
updateList = []
updateDict = {}
needed_updates = []
new_updates = open(f'{tempdir}tvmaze_updates.json', 'r').readlines()
for line in new_updates:
line = line.strip().replace('\n', '').replace('\r', '')
linefield = line.split(":")
updateList.append(linefield[0].replace('"',''))
try:
updateDict[linefield[0].replace('"','')] = linefield[1].replace(',','').strip()
except IndexError:
lprint.logprint("warning", f"Failed to parse line: {line}")
# for result in new_updates:
# updateid = result[0]
# updatestamp = int(result[1])
# tdif = int(tdifference)
# if updatestamp > tdif:
# needed_updates.append(updateid)
# updateList.append(updateid)
config_options = settings.config.Config(ROOTDIR)
options = config_options.config_options
mongo_updater = dbmongo(options)
dbtype, apitype = options["dbtype"], options["apitype"]
lprint.logprint("info", "Creating JSON instance for data retrieval.")
lprint.logprint("info", "JSON instance created.")
lprint.logprint("info", f"Update type is {options['updatetype']}")
timedifference = options["updatetype"]
currenttimestamp = time.time()
if timedifference.lower() == "full":
timedifference = int(currenttimestamp)
basetime = str(currenttimestamp).split(".")
currenttimestamp = int(basetime[0])
ts1 = str(datetime.now()).split(".")[0]
lprint.logprint("info", "Sync start time: " + str(ts1))
print_starttime(lprint, ts1)
lprint.screenprint("info", f"Preparing to load JSON files into MongoDB.")
lprint.screenprint("info", f"Loading JSON files into MongoDB.")
successful, failed , st= load_json_to_mongodb(mongo_updater, directories, updateList, updateDict, lprint)
lprint.screenprint("info", f"JSON loading complete. Successful: {successful}, Failed: {failed}")
lprint.screenprint("info", f"Stats: {st}")
if __name__ == "__main__":
sys.exit(main())
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import json
from pathlib import Path
import db.sql
from jprocessor.process import Jsonload
from jprocessor.process_view import Process_View
import logs.Logger
from db.functions import settype, dbmongo
if __name__ == "__main__":
seriesid = input('Please enter seriesid: ')
#seriesname = input('Please enter series name: ')
ROOTDIR = os.getcwd()
directories = {}
if os.name == 'nt':
LOGDIR = f'{ROOTDIR}\\log'
directories['SERIESDIR'] = f'{ROOTDIR}\\cache\\series\\'
directories['EPISODEDIR'] =f'{ROOTDIR}\\cache\\episodes\\'
directories['CASTDIR'] = f'{ROOTDIR}\\cache\\cast\\'
directories['CREWDIR'] = f'{ROOTDIR}\\cache\\crew\\'
else:
LOGDIR = f'{ROOTDIR}/log'
directories['SERIESDIR'] = f'{ROOTDIR}/cache/series/'
directories['EPISODEDIR'] =f'{ROOTDIR}/cache/episodes/'
directories['CASTDIR'] = f'{ROOTDIR}/cache/cast/'
directories['CREWDIR'] = f'{ROOTDIR}/cache/crew/'
lprint = logs.Logger.lprint(LOGDIR, False)
lprint.startlog()
import settings.config
config_options = settings.config.Config(ROOTDIR)
options = config_options.config_options
mongodb = dbmongo(options)
jload = Jsonload()
viewprocess = Process_View()
#serieslist = mongodb.mgseries.find()
#for series in serieslist:
# print(series)
series = mongodb.mgseries.find_one({"id": int(seriesid)})
episodecursor = mongodb.mgepisodes.find({"seriesid": int(seriesid)})
episodes = []
for episode in episodecursor:
episodes.append(episode)
viewprocess.process_seriesdata(series)
viewprocess.print_episodedata(episodes)
exit()
-138
View File
@@ -1,138 +0,0 @@
#!/usr/bin/env python3
import os
from pymongo import MongoClient
import yaml
import json
import psycopg2
class ColumnMeta:
def __init__(self, name, type, description=None):
self.name = name
self.type = type
self.description = description
def from_dict(value: dict):
return ColumnMeta(name=value['name'], type=value['type'], description=value.get('description'))
class TableMeta:
def __init__(self, name, columns, description=None):
self.name = name
self.columns = columns
self.description = description
def from_dict(value):
columns = [ColumnMeta.from_dict(v) for v in value['columns']]
return TableMeta(name=value['name'], columns=columns, description=value.get('description'))
def from_file(path):
file = open(path, 'r')
yaml_to_dict = yaml.safe_load(file.read())
try:
file.close()
except Exception:
pass
return TableMeta.from_dict(yaml_to_dict)
try:
connection_string = (
(
'postgres://postgres:Optimus0329@192.168.128.7:5432/media_dbsync'
'?options=-csearch_path%3Ddbo,public,updates'
)
)
pgclient = psycopg2.connect(connection_string)
pgcursor = pgclient.cursor()
print(pgclient.get_dsn_parameters(), "\n")
print("Connected to postgresql database server.")
pgcursor.execute("SELECT version();")
record = pgcursor.fetchone()
print("You are connected to - ", record, "\n")
except Exception as e:
print(f"Unable to connect to postgresql database server:{e}\nExitting!")
exit()
host = '192.168.128.8'
port = 27017
print(f"Connecting to mongodb at {host}:{port}")
try:
connection_string = (
f"mongodb://{host}:{port}"
)
mgclient = MongoClient(connection_string)
mgdb = mgclient['test2']
except BaseException as e:
print(f"Unable to connect to mongodb.{e}\nExitting!")
exit()
else:
mgseries = mgdb["series"]
mgseries.create_index('id', unique=True)
mgepisodes = mgdb["episodes"]
mgepisodes.create_index('id', unique=True)
mgactors = mgdb["actors"]
mgactors.create_index('id', unique=True)
mgcharacters = mgdb["characters"]
mgcharacters.create_index('id', unique=True)
mgcrew = mgdb["crew"]
mgcrew.create_index('id', unique=True)
print(
f"Connection to mongodb at {host}:"
f"{port} successful."
)
os.chdir('C:\\Projects\\new_dbsync\\db\\ddl')
# Create the database and tables in PostgreSQL
# Check if the database exists, if not create it
# Check if the tables exist, if not create them
# possibly add , 'genre_ddl.yaml', 'language_ddl.yaml' down the road
ddl_list = [
# 'seriesdata_ddl.yaml',
# 'epdata_ddl.yaml',
# 'actordata_ddl.yaml',
# 'castdata_ddl.yaml',
# 'crewdata_ddl.yaml',
# 'countrydata_ddl.yaml',
# 'networkdata_ddl.yaml',
# 'webchanneldata_ddl.yaml',
# 'temp_seriesdata_ddl.yaml',
# 'temp_epdata_ddl.yaml',
# 'temp_actordata_ddl.yaml',
# 'temp_castdata_ddl.yaml',
# 'temp_crewdata_ddl.yaml',
# 'temp_countrydata_ddl.yaml',
# 'temp_networkdata_ddl.yaml',
# 'temp_webchanneldata_ddl.yaml',
# 'tvupdates_ddl.yaml'
'db_schema.yaml'
]
for ddl in ddl_list:
path = os.path.join(os.getcwd(), ddl)
table_meta = TableMeta.from_file(path)
columnlist = []
for column in table_meta.columns:
columnlist.append(f"{column.name} {column.type}")
table_meta.columns = ', '.join(columnlist)
#print(column.name, column.type)
sql_command = f"DROP TABLE IF EXISTS {table_meta.name};CREATE TABLE IF NOT EXISTS {table_meta.name} ({table_meta.columns})"
#sql_command = f"CREATE TABLE IF NOT EXISTS {table_meta.name} ({table_meta.columns})"
try:
pgcursor.execute(sql_command)
pgclient.commit()
except Exception as e:
print(f"Unable to create table {table_meta.name} in postgresql database server:{e}\nExitting!")
exit()
#print(sql_command)
pgcursor.close()
pgclient.close()
print("PostgreSQL connection is closed")
mgclient.close()
print("MongoDB connection is closed")
exit()
-118
View File
@@ -1,118 +0,0 @@
#!/usr/bin/env python3
import os
from pymongo import MongoClient
import yaml
import json
import psycopg2
class ColumnMeta:
def __init__(self, name, type, description=None):
self.name = name
self.type = type
self.description = description
def from_dict(value: dict):
return ColumnMeta(name=value['name'], type=value['type'], description=value.get('description'))
class TableMeta:
def __init__(self, name, columns, description=None):
self.name = name
self.columns = columns
self.description = description
def from_dict(value):
columns = [ColumnMeta.from_dict(v) for v in value['columns']]
return TableMeta(name=value['name'], columns=columns, description=value.get('description'))
def from_file(path):
file = open(path, 'r')
yaml_to_dict = yaml.safe_load(file.read())
try:
file.close()
except Exception:
pass
return TableMeta.from_dict(yaml_to_dict)
try:
connection_string = (
(
'postgres://postgres:Optimus0329@192.168.128.7:5432/media_dbsync'
'?options=-csearch_path%3Ddbo,public,updates'
)
)
pgclient = psycopg2.connect(connection_string)
pgcursor = pgclient.cursor()
print(pgclient.get_dsn_parameters(), "\n")
print("Connected to postgresql database server.")
pgcursor.execute("SELECT version();")
record = pgcursor.fetchone()
print("You are connected to - ", record, "\n")
except Exception as e:
print(f"Unable to connect to postgresql database server:{e}\nExitting!")
exit()
host = '192.168.128.8'
port = 27017
print(f"Connecting to mongodb at {host}:{port}")
try:
connection_string = (
f"mongodb://{host}:{port}"
)
mgclient = MongoClient(connection_string)
mgdb = mgclient['test2']
except BaseException as e:
print(f"Unable to connect to mongodb.{e}\nExitting!")
exit()
else:
mgseries = mgdb["series"]
mgseries.create_index('id', unique=True)
mgepisodes = mgdb["episodes"]
mgepisodes.create_index('id', unique=True)
mgactors = mgdb["actors"]
mgactors.create_index('id', unique=True)
mgcharacters = mgdb["characters"]
mgcharacters.create_index('id', unique=True)
mgcrew = mgdb["crew"]
mgcrew.create_index('id', unique=True)
print(
f"Connection to mongodb at {host}:"
f"{port} successful."
)
os.chdir('C:\\Projects\\new_dbsync\\db\\ddl')
# Create the database and tables in PostgreSQL
# Check if the database exists, if not create it
# Check if the tables exist, if not create them
# possibly add , 'genre_ddl.yaml', 'language_ddl.yaml' down the road
ddl_list = os.listdir(os.getcwd())
for ddl in ddl_list:
path = os.path.join(os.getcwd(), ddl)
table_meta = TableMeta.from_file(path)
columnlist = []
for column in table_meta.columns:
columnlist.append(f"{column.name} {column.type}")
table_meta.columns = ', '.join(columnlist)
#print(column.name, column.type)
sql_command = f"DROP TABLE IF EXISTS {table_meta.name};CREATE TABLE IF NOT EXISTS {table_meta.name} ({table_meta.columns})"
#sql_command = f"CREATE TABLE IF NOT EXISTS {table_meta.name} ({table_meta.columns})"
try:
pgcursor.execute(sql_command)
pgclient.commit()
except Exception as e:
print(f"Unable to create table {table_meta.name} in postgresql database server:{e}\nExitting!")
exit()
#print(sql_command)
pgcursor.close()
pgclient.close()
print("PostgreSQL connection is closed")
mgclient.close()
print("MongoDB connection is closed")
exit()