Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fa8b83f2d | |||
| afe2dfa1c8 | |||
| c6f9d1e617 | |||
| 179fb4fddb | |||
| 8ed2a77365 | |||
| 1283bd282a | |||
| 9fbd5dc8b9 | |||
| a52bd5571e | |||
| a49354bbcb | |||
| 66cb23bb6d | |||
| 0f0f9078c6 | |||
| 7e98f01cd2 | |||
| c6038c0b01 | |||
| 2d414ecbfc | |||
| 332ad8e5d8 | |||
| df89aad7e6 | |||
| bbf92af79f | |||
| 26a7e94b4e | |||
| 968a3a2949 | |||
| f6254ee42e | |||
| f5627e58a8 | |||
| 5c97eb3fb4 | |||
| f79f8772ae | |||
| 99a2d78032 | |||
| 3447ba369f | |||
| eb1615ea3d | |||
| 4f0267ecf2 | |||
| 097225c942 | |||
| 24203aff66 | |||
| 3bc35a08f9 | |||
| 7dfe22fe71 | |||
| e975b947fa | |||
| a18c6a7386 | |||
| 2c2d6da194 | |||
| 96bd323567 | |||
| 197a81d8fb | |||
| dfeb5cb36e | |||
| aa2001bc87 | |||
| c5502711e7 | |||
| c51150396c | |||
| b390fc9f9a | |||
| 7837ca2dd4 | |||
| 271bf606ca | |||
| 1d14e97637 | |||
| b9f135e325 |
+14
-2
@@ -5,11 +5,23 @@
|
||||
/.vs/new_dbsync/FileContentIndex
|
||||
/.vs
|
||||
*.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
|
||||
*.log
|
||||
/pyproject.toml
|
||||
dbsync.log.*
|
||||
.idea
|
||||
/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
|
||||
|
||||
@@ -1,8 +1,162 @@
|
||||
# DBSyncer
|
||||
# DBSyncer - MongoDB to PostgreSQL Data Synchronization
|
||||
|
||||
This project has two parts:
|
||||
1.) it uses the tvmaze API to download Television dat and store it in a mongodb database.
|
||||
2.) Takes saved json data and inserts/updates data in a postgresql database.
|
||||
A comprehensive data synchronization tool that:
|
||||
1. Downloads Television data from the TVMaze API and stores it in MongoDB
|
||||
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.
|
||||
|
||||
+6
-7
@@ -250,7 +250,6 @@ class CreateTempTables():
|
||||
pass
|
||||
self.create(Base, engine, metadata, session, self.tablelist)
|
||||
session.commit()
|
||||
#self.send(tablelist)
|
||||
|
||||
def settables(self, metadata, engine):
|
||||
tablelist = []
|
||||
@@ -379,12 +378,12 @@ class CreateTempTables():
|
||||
schema = "updates",
|
||||
extend_existing=True)
|
||||
tablelist.append(countries)
|
||||
tvupdates = Table('tvupdates', metadata,
|
||||
Column('seriesid', BIGINT, primary_key = True),
|
||||
Column('timestamp', VARCHAR(15)),
|
||||
schema = "updates",
|
||||
extend_existing=True)
|
||||
tablelist.append(tvupdates)
|
||||
# tvupdates = Table('tvupdates', metadata,
|
||||
# Column('seriesid', BIGINT, primary_key = True),
|
||||
# Column('timestamp', VARCHAR(15)),
|
||||
# schema = "updates",
|
||||
# extend_existing=True)
|
||||
# tablelist.append(tvupdates)
|
||||
return(tablelist)
|
||||
|
||||
def create(self, Base, engine, metadata, session, tablelist):
|
||||
|
||||
+83
-126
@@ -19,8 +19,9 @@ from db.pgsql.Merger import Episodemerge as PGEpisodemerge
|
||||
import api.tvmaze.episodes
|
||||
import api.tvmaze.cast
|
||||
import api.tvmaze.series
|
||||
from pymongo import MongoClient
|
||||
from pymongo import MongoClient, ReplaceOne, InsertOne
|
||||
from tqdm import tqdm
|
||||
from datetime import datetime
|
||||
|
||||
Createtables = Createtables
|
||||
CreateTempTables = CreateTempTables
|
||||
@@ -113,23 +114,18 @@ class settype(object):
|
||||
)
|
||||
currentDir = os.path.dirname(os.path.abspath(__file__))
|
||||
currentDir = os.path.dirname(currentDir)
|
||||
ddl_file_path = f"{currentDir}/db/ddl/recreate_updates_schema.sql"
|
||||
with open(ddl_file_path, "r") as file:
|
||||
sql = file.read()
|
||||
file
|
||||
with engine.connect() as conn:
|
||||
with conn as cursor:
|
||||
cursor.execute(text(sql))
|
||||
# conn.commit()
|
||||
# ddl_file_path = f"{currentDir}/db/ddl/recreate_updates_schema.sql"
|
||||
# with open(ddl_file_path, "r") as file:
|
||||
# sql = file.read()
|
||||
# file
|
||||
# with engine.connect() as conn:
|
||||
# conn.execute(text("CREATE SCHEMA IF NOT EXISTS updates"))
|
||||
# conn.execute(text("CREATE SCHEMA IF NOT EXISTS dbo"))
|
||||
# conn.commit()
|
||||
# with conn as cursor:
|
||||
# cursor.execute(text(sql))
|
||||
updatesBase.prepare(engine, reflect=True, schema="updates")
|
||||
dboBase.prepare(engine, reflect=True, schema="dbo")
|
||||
metadata = sqlalchemy.MetaData("updates")
|
||||
metadata = sqlalchemy.MetaData(schema="updates")
|
||||
metadata.reflect(bind=engine)
|
||||
dboMetadata = sqlalchemy.MetaData("dbo")
|
||||
dboMetadata = sqlalchemy.MetaData(schema="dbo")
|
||||
dboMetadata.reflect(bind=engine)
|
||||
session = Session(engine)
|
||||
self.dbengine["engine"] = engine
|
||||
@@ -170,6 +166,25 @@ class dbmongo(object):
|
||||
global mgcharacters
|
||||
global mgcrew
|
||||
|
||||
def bulk_upsert_by_id(self, collection, docs, batch_size=1000):
|
||||
ops = []
|
||||
for doc in docs:
|
||||
if "id" not in doc:
|
||||
# skip documents without an `id` field
|
||||
continue
|
||||
# replace existing document with same `id`, or insert if it does not exist
|
||||
ops.append(ReplaceOne({"id": doc["id"]}, doc, upsert=True))
|
||||
if len(ops) >= batch_size:
|
||||
collection.bulk_write(ops, ordered=False)
|
||||
ops = []
|
||||
if ops:
|
||||
collection.bulk_write(ops, ordered=False)
|
||||
|
||||
def convert_updated_time(self, timestamp):
|
||||
new_timestamp = datetime.fromtimestamp(timestamp)
|
||||
update_time = str(new_timestamp).split(".")[0]
|
||||
return update_time
|
||||
|
||||
def __init__(self, options):
|
||||
host = options["mghostname"]
|
||||
port = options["mgport"]
|
||||
@@ -210,8 +225,6 @@ class dbmongo(object):
|
||||
lprint.logprint(
|
||||
"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(
|
||||
self,
|
||||
@@ -233,6 +246,8 @@ class dbmongo(object):
|
||||
"info", f"Unable to process series file for {seriesid}"
|
||||
)
|
||||
else:
|
||||
update_time = self.convert_updated_time(seriesdata["updated"])
|
||||
seriesdata["updated"] = update_time
|
||||
self.update_country_data(
|
||||
country,
|
||||
countrydictionary,
|
||||
@@ -260,79 +275,17 @@ class dbmongo(object):
|
||||
dboBase,
|
||||
lprint,
|
||||
)
|
||||
#self.bulk_upsert_by_id(self.mgseries, seriesdata)
|
||||
self.insert_or_update_mongo(
|
||||
self.mgseries, seriesdata, "id"
|
||||
self.mgseries, seriesdata, "id"
|
||||
)
|
||||
|
||||
def process_cast_data(self, persondata, characterdata, seriesid, lprint):
|
||||
try:
|
||||
aupserts = [
|
||||
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
|
||||
]
|
||||
self.bulk_upsert_by_id(self.mgactors, persondata)
|
||||
self.bulk_upsert_by_id(self.mgcharacters, characterdata)
|
||||
|
||||
def process_crew_data(self, crewdata, seriesid, jget, lprint):
|
||||
try:
|
||||
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
|
||||
]
|
||||
self.bulk_upsert_by_id(self.mgcrew, crewdata)
|
||||
|
||||
def update_country_data(
|
||||
self, country, countrydictionary, dbExec, dbengine, dbBase,
|
||||
@@ -374,8 +327,6 @@ class dbmongo(object):
|
||||
dbExec.rawsql_insert(
|
||||
dbengine["engine"], insert_sql, lprint
|
||||
)
|
||||
# seriesresults['countryid'] = 'No Country'
|
||||
# return seriesresults['countryid']
|
||||
|
||||
def update_network_data(
|
||||
self,
|
||||
@@ -426,19 +377,9 @@ class dbmongo(object):
|
||||
del new_values["$set"]["_id"]
|
||||
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(
|
||||
self,
|
||||
updateList,
|
||||
ROOTDIR,
|
||||
directories,
|
||||
jget,
|
||||
countrydictionary,
|
||||
@@ -456,7 +397,7 @@ class dbmongo(object):
|
||||
failed_file_count = 0
|
||||
|
||||
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."""
|
||||
data = self.load_json_file(
|
||||
@@ -475,33 +416,25 @@ class dbmongo(object):
|
||||
persondata = []
|
||||
characterdata = []
|
||||
for item in data:
|
||||
update_time = self.convert_updated_time(item["person"]["updated"])
|
||||
item["person"]["updated"] = update_time
|
||||
if "person" in item:
|
||||
item["person"]["seriesid"] = seriesid
|
||||
persondata.append(item["person"])
|
||||
if "character" in item:
|
||||
item["character"]["seriesid"] = seriesid
|
||||
characterdata.append(item["character"])
|
||||
try:
|
||||
for item in persondata:
|
||||
item["seriesid"] = seriesid
|
||||
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
|
||||
)
|
||||
self.bulk_upsert_by_id(self.mgactors, persondata)
|
||||
self.bulk_upsert_by_id(self.mgcharacters, characterdata)
|
||||
failed_file_count = "N/A"
|
||||
return failed_file_count
|
||||
except Exception as e:
|
||||
failed_file_count = 1
|
||||
lprint.logprint(
|
||||
"info",
|
||||
f"Unable to process {data_type} data. {e}"
|
||||
)
|
||||
return failed_file_count
|
||||
if "crew" in file_path:
|
||||
try:
|
||||
crewdata = []
|
||||
@@ -512,11 +445,15 @@ class dbmongo(object):
|
||||
)
|
||||
else:
|
||||
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:
|
||||
crewdata.append(item["person"])
|
||||
try:
|
||||
process_method(crewdata, seriesid, jget, lprint)
|
||||
self.bulk_upsert_by_id(self.mgcrew, crewdata)
|
||||
failed_file_count = "N/A"
|
||||
return failed_file_count
|
||||
except Exception as e:
|
||||
@@ -526,7 +463,7 @@ class dbmongo(object):
|
||||
f"Unable to process {data_type} data.{e}"
|
||||
)
|
||||
return failed_file_count
|
||||
|
||||
return data
|
||||
for series in tqdm(
|
||||
range(len(updateList)), desc="Update MongoDB", unit="series"
|
||||
):
|
||||
@@ -552,9 +489,7 @@ class dbmongo(object):
|
||||
countrydictionary,
|
||||
seriesid,
|
||||
)
|
||||
self.insert_or_update_mongo(
|
||||
self.mgseries, seriesdata, "id"
|
||||
)
|
||||
self.bulk_upsert_by_id(self.mgseries, seriesdata)
|
||||
except Exception as e:
|
||||
series_failed_count += 1
|
||||
lprint.logprint(
|
||||
@@ -567,33 +502,55 @@ class dbmongo(object):
|
||||
else:
|
||||
processed_count += 1
|
||||
|
||||
# Process episode, cast, and crew data using the helper function
|
||||
fcount = process_data(
|
||||
episodecachefile,
|
||||
"episode",
|
||||
self.process_episode_data,
|
||||
failed_file_count,
|
||||
lprint,
|
||||
)
|
||||
if isinstance(fcount, int):
|
||||
episode_failed_count = episode_failed_count + fcount
|
||||
else:
|
||||
try:
|
||||
self.bulk_upsert_by_id(self.mgepisodes, fcount)
|
||||
# for episode in fcount:
|
||||
# self.insert_or_update_mongo(
|
||||
# self.mgepisodes, episode, "id"
|
||||
# )
|
||||
except Exception as e:
|
||||
lprint.logprint(
|
||||
"info",
|
||||
(
|
||||
f"Unable to insert episode data for seriesid "
|
||||
f"{seriesid}. {e}"
|
||||
),
|
||||
)
|
||||
fcount = process_data(
|
||||
castcachefile, "cast", self.process_cast_data,
|
||||
failed_file_count, lprint
|
||||
castcachefile, "cast",failed_file_count, lprint
|
||||
)
|
||||
if isinstance(fcount, int):
|
||||
cast_failed_count = cast_failed_count + fcount
|
||||
try:
|
||||
if os.path.isfile(crewcachefile):
|
||||
fcount = process_data(
|
||||
crewcachefile, "crew", self.process_crew_data,
|
||||
crewcachefile, "crew",
|
||||
failed_file_count, lprint
|
||||
)
|
||||
except Exception:
|
||||
crew_failed_count = crew_failed_count + 1
|
||||
else:
|
||||
if isinstance(fcount, int):
|
||||
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"Failed to load {series_failed_count} series files.")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -12,23 +12,32 @@ class Dbexec():
|
||||
def update_tvupdates(self, engine, available_updates, tablename):
|
||||
print('Creating update transaction.')
|
||||
|
||||
# Build batch insert data
|
||||
updatelist = [
|
||||
{
|
||||
'seriesid': seriesid,
|
||||
'seriesid': str(seriesid),
|
||||
'timestamp': available_updates[seriesid]
|
||||
}
|
||||
for seriesid in tqdm(
|
||||
available_updates,
|
||||
desc='Updating tvupdates table',
|
||||
desc='Preparing tvupdates records',
|
||||
unit='record'
|
||||
)
|
||||
]
|
||||
|
||||
print('Applying updates to table.')
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text(f"truncate table {tablename}"))
|
||||
conn.execute(tablename.insert(), updatelist)
|
||||
conn.commit()
|
||||
# Use a transactional context; SQLAlchemy Connection.execute accepts
|
||||
# a list of parameter mappings for bulk inserts (it will use the DBAPI
|
||||
# executemany under the hood).
|
||||
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.')
|
||||
|
||||
def rawsql_select(self, engine, sqlquery, lprint):
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -1,95 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import logging
|
||||
|
||||
|
||||
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):
|
||||
def download_json_to_file(api_url, outputdir, output_file, lprint):
|
||||
"""
|
||||
Downloads JSON data from the given API URL and saves it to the specified file.
|
||||
"""
|
||||
try:
|
||||
with open(jfile, 'w') as datafile:
|
||||
datafile.write(jdata)
|
||||
except Exception:
|
||||
print('Unable to save cache file.')
|
||||
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:
|
||||
response = requests.get(api_url)
|
||||
response.raise_for_status() # Raise an error for bad status codes
|
||||
data = response.json()
|
||||
with open(f"{outputdir}{output_file}", 'w') as f:
|
||||
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 = []
|
||||
listing = open('D:/Temp/filelist.txt', 'w')
|
||||
def main():
|
||||
# Download data from an API endpoint to a specified file in JSON format
|
||||
api_url = 'https://sample-api.com/data'
|
||||
outputdir = '/home/user/'
|
||||
outputfile = 'data.json'
|
||||
|
||||
os.chdir('D:/Projects/new_dbsync/cache/series')
|
||||
# replace with your directory path
|
||||
directory_path = 'D:/Projects/new_dbsync/cache/series'
|
||||
files = [
|
||||
f for f in os.listdir(directory_path)
|
||||
if os.path.isfile(os.path.join(directory_path, f))
|
||||
]
|
||||
for filename in files:
|
||||
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()
|
||||
lprint = Logprint('output')
|
||||
try:
|
||||
download_json_to_file(api_url, outputdir, outputfile)
|
||||
print("Download successful!")
|
||||
except Exception as e:
|
||||
# Handle any exceptions that occur during the download process and log them
|
||||
lprint.logprint("info", f"Downloading {outputdir}{outputfile} failed with error:{e}")
|
||||
exit(1)
|
||||
|
||||
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
@@ -44,12 +44,12 @@ class LPrint:
|
||||
"""Check the log file size and rotate if necessary."""
|
||||
log_size = os.path.getsize(self.logfile)
|
||||
print(f'Logfile Size: {log_size} ({self.humanbytes(log_size)})')
|
||||
if force_rotate or log_size > 10_000_000: # 10 MB
|
||||
if force_rotate or log_size > 50_000_000: # 10 MB
|
||||
self.rotate_logs()
|
||||
|
||||
def rotate_logs(self):
|
||||
"""Rotate the log file."""
|
||||
from logrotater import LogRotate # Assuming logrotater is a custom module
|
||||
from logs.logrotater import LogRotate # Assuming logrotater is a custom module
|
||||
rotater = LogRotate(prefix=self.logfile, verbose=True)
|
||||
try:
|
||||
rotater.rotate()
|
||||
@@ -79,6 +79,23 @@ class LPrint:
|
||||
else:
|
||||
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):
|
||||
"""Create a rotating log handler."""
|
||||
logger = logging.getLogger("Rotating Log")
|
||||
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
logrotater==1.3
|
||||
numpy==2.2.3
|
||||
progressbar33==2.4
|
||||
psycopg2==2.9.10
|
||||
psycopg2-binary==2.9.10
|
||||
pymongo==4.11.2
|
||||
pymssql==2.3.2
|
||||
Requests==2.32.3
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /srv/Projects/new_dbsync
|
||||
/srv/Projects/new_dbsync/update_json.py >> /home/wjones/update_mongo.log
|
||||
@@ -61,3 +61,58 @@ class Config(object):
|
||||
def __del__(self):
|
||||
|
||||
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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[global]
|
||||
loglevel=debug
|
||||
loglevel=info
|
||||
|
||||
[dbsettings]
|
||||
sqlserver_driver=ODBC Driver 13 for SQL Server
|
||||
hostname=192.168.128.7
|
||||
mghostname=192.168.128.8
|
||||
hostname=192.168.128.3
|
||||
mghostname=192.168.128.24
|
||||
dbname=media_dbsync
|
||||
mgdbname=test2
|
||||
mgdbname=tvdata
|
||||
mysqlusername=root
|
||||
pgsqlusername=postgres
|
||||
mssqlusername=sa
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
Executable
+305
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user