Refactor README.md for clarity and detail: enhance project description, add features, usage examples, and troubleshooting sections; remove outdated mongo2pgsql.py and search_mongodb.py files
This commit is contained in:
@@ -1,8 +1,162 @@
|
|||||||
# DBSyncer
|
# DBSyncer - MongoDB to PostgreSQL Data Synchronization
|
||||||
|
|
||||||
This project has two parts:
|
A comprehensive data synchronization tool that:
|
||||||
1.) it uses the tvmaze API to download Television dat and store it in a mongodb database.
|
1. Downloads Television data from the TVMaze API and stores it in MongoDB
|
||||||
2.) Takes saved json data and inserts/updates data in a postgresql database.
|
2. Automatically generates PostgreSQL table schemas from MongoDB document structures
|
||||||
|
3. Efficiently inserts/updates MongoDB documents into PostgreSQL with intelligent type conversion
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
[Learn more about creating GitLab projects.](https://docs.gitlab.com/ee/gitlab-basics/create-project.html)
|
### Schema Generation (`db/schema_generator.py`)
|
||||||
|
- **Automatic Schema Inference**: Analyzes MongoDB documents to infer PostgreSQL types
|
||||||
|
- **Type Mapping**: Intelligent conversion between MongoDB and PostgreSQL types:
|
||||||
|
- Objects/Arrays → JSONB
|
||||||
|
- Dates → TIMESTAMP
|
||||||
|
- Numbers → BIGINT / DOUBLE PRECISION
|
||||||
|
- Strings → TEXT
|
||||||
|
|
||||||
|
### Document Insertion
|
||||||
|
- **Batch Processing**: Efficiently inserts documents in configurable batches (default: 1000)
|
||||||
|
- **Type Conversion**: Automatic type conversion for:
|
||||||
|
- Epoch timestamps ↔ ISO datetime strings
|
||||||
|
- JSON strings ↔ Python dict/list
|
||||||
|
- ObjectId → Text
|
||||||
|
- **UPSERT Support**: Uses PostgreSQL `ON CONFLICT ... DO UPDATE SET` for automatic upserts
|
||||||
|
- **Error Handling**: Detailed logging with per-row error reporting
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
Edit `settings/tvsync_settings.cfg`:
|
||||||
|
```ini
|
||||||
|
[dbsettings]
|
||||||
|
hostname = localhost
|
||||||
|
pgsqlport = 5432
|
||||||
|
pgsqlusername = postgres
|
||||||
|
pgsqlpassword = your_password
|
||||||
|
dbname = media_dbsync
|
||||||
|
mghostname = localhost
|
||||||
|
mgport = 27017
|
||||||
|
mgdbname = media
|
||||||
|
|
||||||
|
[database]
|
||||||
|
dbtype = pgsql
|
||||||
|
updateschema = updates
|
||||||
|
```
|
||||||
|
|
||||||
|
### Main Scripts
|
||||||
|
|
||||||
|
#### `update_mongodb.py`
|
||||||
|
Fetches TV data from TVMaze API and stores in MongoDB:
|
||||||
|
```bash
|
||||||
|
python3 update_mongodb.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `mongodb2postgres.py`
|
||||||
|
Generates PostgreSQL schemas from MongoDB collections and inserts documents:
|
||||||
|
```bash
|
||||||
|
python3 mongodb2postgres.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
1. Analyze each MongoDB collection (series, episodes, actors, characters, crew)
|
||||||
|
2. Automatically create corresponding PostgreSQL tables in the `updates` schema
|
||||||
|
3. Insert/upsert all documents with proper type conversions
|
||||||
|
4. Report progress and any errors
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Generate Table Schema from MongoDB Collection
|
||||||
|
```python
|
||||||
|
from db.schema_generator import MongoToPostgresSchemaGenerator
|
||||||
|
|
||||||
|
generator = MongoToPostgresSchemaGenerator(sample_size=100)
|
||||||
|
schema = generator.analyze_collection(mongo_db, 'series')
|
||||||
|
|
||||||
|
# View the SQL
|
||||||
|
sql = generator.generate_create_table_sql('seriesdata', schema_name='dbo', pk_field='id')
|
||||||
|
print(sql)
|
||||||
|
|
||||||
|
# Create the table
|
||||||
|
generator.create_table_in_postgres(engine, 'seriesdata', schema_name='dbo', pk_field='id')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert Documents with Type Conversion
|
||||||
|
```python
|
||||||
|
from db.schema_generator import MongoDocumentInserter
|
||||||
|
|
||||||
|
inserter = MongoDocumentInserter(batch_size=1000)
|
||||||
|
|
||||||
|
# From a collection
|
||||||
|
count = inserter.insert_from_collection(
|
||||||
|
engine=engine,
|
||||||
|
table_name='seriesdata',
|
||||||
|
collection=mongo_db['series'],
|
||||||
|
schema_name='dbo',
|
||||||
|
on_conflict="ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, updated=EXCLUDED.updated",
|
||||||
|
)
|
||||||
|
print(f"Inserted {count} documents")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Database Classes (`db/functions.py`)
|
||||||
|
- `settype`: Manages database engine creation and API configuration
|
||||||
|
- `dbmongo`: Handles MongoDB connections and operations
|
||||||
|
- `Dbexec`: Executes raw SQL queries and batch updates
|
||||||
|
|
||||||
|
### Schema Generator (`db/schema_generator.py`)
|
||||||
|
- `MongoToPostgresSchemaGenerator`: Infers and creates table schemas
|
||||||
|
- `MongoDocumentInserter`: Handles document insertion with type conversion
|
||||||
|
|
||||||
|
### API Integration (`api/`)
|
||||||
|
- TVMaze API client for fetching show, episode, cast, and crew data
|
||||||
|
- TheTVDB API client (optional)
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
- **Batch Inserts**: Uses SQLAlchemy's parameterized queries for efficient batch operations
|
||||||
|
- **Transactional Inserts**: Groups inserts in transactions to reduce database overhead
|
||||||
|
- **Column Type Awareness**: Converts data to match target column types, avoiding casting overhead
|
||||||
|
- **Connection Pooling**: Reuses database connections (pool_size=20, max_overflow=20)
|
||||||
|
- **UPSERT Operations**: Uses PostgreSQL's `ON CONFLICT` for atomic insert-or-update
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### MetaData Schema Argument Error
|
||||||
|
If you see `sqlalchemy.exc.ArgumentError: Could not parse SQLAlchemy URL`, ensure:
|
||||||
|
- Use `MetaData(schema="updates")` instead of `MetaData("updates")`
|
||||||
|
- Pass full connection strings to `create_engine()`, not schema names
|
||||||
|
|
||||||
|
### Type Conversion Errors
|
||||||
|
The inserter automatically handles:
|
||||||
|
- Datetime strings → epoch timestamps (for BIGINT columns)
|
||||||
|
- Epoch ints → datetime objects (for TIMESTAMP columns)
|
||||||
|
- JSON strings → Python dicts (for JSONB columns)
|
||||||
|
|
||||||
|
If type errors persist, check that:
|
||||||
|
1. Table schema matches MongoDB document fields
|
||||||
|
2. Column types are correctly inferred from sample documents
|
||||||
|
3. Use appropriate `on_conflict` clauses for upserts
|
||||||
|
|
||||||
|
### Column Name Case Sensitivity
|
||||||
|
PostgreSQL stores unquoted identifiers as lowercase. The inserter automatically:
|
||||||
|
- Lowercases column names in `ON CONFLICT` clauses
|
||||||
|
- Uses case-insensitive matching when mapping MongoDB fields to PostgreSQL columns
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When modifying:
|
||||||
|
- **Schema inference**: Update `MongoToPostgresSchemaGenerator.TYPE_MAPPING` for new type support
|
||||||
|
- **Type conversion**: Extend `MongoDocumentInserter.convert_value()` for custom conversions
|
||||||
|
- **Database operations**: Use parameterized queries via `text()` to prevent SQL injection
|
||||||
|
- **Error handling**: Add specific exception types to improve debugging
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See LICENSE file for details.
|
||||||
|
|||||||
-205
@@ -1,205 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Convert a MongoDB collection into a PostgreSQL table.
|
|
||||||
|
|
||||||
Contains `convert_mongo_to_pg(mongo_uri, mongo_db, collection_name, pg_dsn, pg_table=None, ...)`
|
|
||||||
and a small CLI.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Uses `pymongo` to read documents and `psycopg2` to write to Postgres.
|
|
||||||
- Infers basic column types from a sample of documents; nested objects/arrays
|
|
||||||
are stored as `JSONB`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from typing import Any, Dict, Iterable, List, Optional
|
|
||||||
|
|
||||||
import pymongo
|
|
||||||
import psycopg2
|
|
||||||
import psycopg2.extras as pgextras
|
|
||||||
import psycopg2.sql as sql
|
|
||||||
|
|
||||||
LOG = logging.getLogger("mongo2pgsql")
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_type(value: Any) -> str:
|
|
||||||
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 "real"
|
|
||||||
if isinstance(value, (dict, list)):
|
|
||||||
return "jsonb"
|
|
||||||
return "text"
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_types(types: List[str]) -> str:
|
|
||||||
# priority: jsonb > text > real > integer > boolean > null
|
|
||||||
s = set(types)
|
|
||||||
if "jsonb" in s:
|
|
||||||
return "jsonb"
|
|
||||||
if "text" in s:
|
|
||||||
return "text"
|
|
||||||
if "real" in s:
|
|
||||||
return "real"
|
|
||||||
if "integer" in s:
|
|
||||||
return "integer"
|
|
||||||
if "boolean" in s:
|
|
||||||
return "boolean"
|
|
||||||
return "text"
|
|
||||||
|
|
||||||
|
|
||||||
def _pg_type_for(kind: str) -> str:
|
|
||||||
return {
|
|
||||||
"integer": "BIGINT",
|
|
||||||
"real": "DOUBLE PRECISION",
|
|
||||||
"boolean": "BOOLEAN",
|
|
||||||
"jsonb": "JSONB",
|
|
||||||
"text": "TEXT",
|
|
||||||
}.get(kind, "TEXT")
|
|
||||||
|
|
||||||
|
|
||||||
def convert_mongo_to_pg(
|
|
||||||
mongo_uri: str,
|
|
||||||
mongo_db: str,
|
|
||||||
collection_name: str,
|
|
||||||
pg_dsn: str,
|
|
||||||
pg_table: Optional[str] = None,
|
|
||||||
sample_size: int = 500,
|
|
||||||
batch_size: int = 1000,
|
|
||||||
create_table: bool = True,
|
|
||||||
pk_field: str = "_id",
|
|
||||||
) -> None:
|
|
||||||
"""Copy a MongoDB collection to PostgreSQL.
|
|
||||||
|
|
||||||
- `pg_table` defaults to `collection_name` if omitted.
|
|
||||||
- Nested documents/arrays are stored as JSONB.
|
|
||||||
- `_id` is stored as TEXT by default.
|
|
||||||
"""
|
|
||||||
if pg_table is None:
|
|
||||||
pg_table = collection_name
|
|
||||||
|
|
||||||
client = pymongo.MongoClient(mongo_uri)
|
|
||||||
coll = client[mongo_db][collection_name]
|
|
||||||
|
|
||||||
sample = list(coll.find({}, projection=None, limit=sample_size))
|
|
||||||
if not sample:
|
|
||||||
raise ValueError("collection is empty or not found")
|
|
||||||
|
|
||||||
# gather all field names and observed types
|
|
||||||
field_types: Dict[str, List[str]] = {}
|
|
||||||
for doc in sample:
|
|
||||||
for k, v in doc.items():
|
|
||||||
t = _detect_type(v)
|
|
||||||
field_types.setdefault(k, []).append(t)
|
|
||||||
|
|
||||||
# finalize types
|
|
||||||
final_types: Dict[str, str] = {}
|
|
||||||
for k, types in field_types.items():
|
|
||||||
merged = _merge_types(types)
|
|
||||||
final_types[k] = _pg_type_for(merged)
|
|
||||||
|
|
||||||
# ensure primary key present
|
|
||||||
if pk_field not in final_types:
|
|
||||||
final_types[pk_field] = "TEXT"
|
|
||||||
|
|
||||||
cols = list(final_types.items())
|
|
||||||
|
|
||||||
# create table
|
|
||||||
if create_table:
|
|
||||||
# Build CREATE TABLE statement using psycopg2.sql for safe identifiers
|
|
||||||
col_defs = []
|
|
||||||
for name, ptype in cols:
|
|
||||||
col_defs.append(sql.SQL("{} {}").format(sql.Identifier(name), sql.SQL(ptype)))
|
|
||||||
|
|
||||||
create_body = sql.SQL(", ").join(col_defs)
|
|
||||||
if pk_field in final_types:
|
|
||||||
pk_clause = sql.SQL(", PRIMARY KEY ({})").format(sql.SQL(",").join(map(sql.Identifier, [pk_field])))
|
|
||||||
create_body = create_body + pk_clause
|
|
||||||
|
|
||||||
create_stmt = sql.SQL("CREATE TABLE IF NOT EXISTS {} ({})").format(sql.Identifier(pg_table), create_body)
|
|
||||||
with psycopg2.connect(pg_dsn) as pgconn:
|
|
||||||
with pgconn.cursor() as cur:
|
|
||||||
LOG.info("Creating table %s", pg_table)
|
|
||||||
cur.execute(create_stmt)
|
|
||||||
pgconn.commit()
|
|
||||||
|
|
||||||
# stream documents and insert
|
|
||||||
insert_cols = [n for n, _ in cols]
|
|
||||||
|
|
||||||
with psycopg2.connect(pg_dsn) as pgconn:
|
|
||||||
with pgconn.cursor() as cur:
|
|
||||||
# build INSERT statement for execute_values: it expects a single %s
|
|
||||||
base_insert = sql.SQL("INSERT INTO {} ({}) VALUES %s").format(
|
|
||||||
sql.Identifier(pg_table),
|
|
||||||
sql.SQL(', ').join(map(sql.Identifier, insert_cols)),
|
|
||||||
).as_string(pgconn)
|
|
||||||
batch = []
|
|
||||||
# Use an explicit session when using no_cursor_timeout to avoid pymongo warning
|
|
||||||
with client.start_session() as session:
|
|
||||||
cursor = coll.find({}, no_cursor_timeout=True, session=session).batch_size(batch_size)
|
|
||||||
try:
|
|
||||||
for doc in cursor:
|
|
||||||
row = []
|
|
||||||
for col_name in insert_cols:
|
|
||||||
v = doc.get(col_name)
|
|
||||||
if v is None:
|
|
||||||
row.append(None)
|
|
||||||
continue
|
|
||||||
# convert ObjectId and other non-JSON types
|
|
||||||
if isinstance(v, (dict, list)):
|
|
||||||
row.append(pgextras.Json(v))
|
|
||||||
else:
|
|
||||||
row.append(str(v) if col_name == pk_field else v)
|
|
||||||
batch.append(tuple(row))
|
|
||||||
if len(batch) >= batch_size:
|
|
||||||
pgextras.execute_values(cur, base_insert, batch, template=None, page_size=batch_size)
|
|
||||||
pgconn.commit()
|
|
||||||
batch = []
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
cursor.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if batch:
|
|
||||||
pgextras.execute_values(cur, base_insert, batch, template=None, page_size=len(batch))
|
|
||||||
pgconn.commit()
|
|
||||||
|
|
||||||
LOG.info("Finished copying collection %s.%s to %s", mongo_db, collection_name, pg_table)
|
|
||||||
|
|
||||||
|
|
||||||
def _cli() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="Copy a MongoDB collection to PostgreSQL")
|
|
||||||
parser.add_argument("mongo_uri", help="MongoDB connection URI, e.g. mongodb://user:pw@host:27017")
|
|
||||||
parser.add_argument("mongo_db", help="MongoDB database name")
|
|
||||||
parser.add_argument("collection", help="Collection name to copy")
|
|
||||||
parser.add_argument("pg_dsn", help="Postgres DSN, e.g. \"host=.. dbname=.. user=.. password=..\"")
|
|
||||||
parser.add_argument("--pg-table", help="Target Postgres table name (defaults to collection name)")
|
|
||||||
parser.add_argument("--sample", type=int, default=500, help="Sample size to infer schema")
|
|
||||||
parser.add_argument("--batch", type=int, default=1000, help="Insert batch size")
|
|
||||||
parser.add_argument("--no-create", dest="create", action="store_false", help="Don't attempt to create table in PG")
|
|
||||||
parser.add_argument("--verbose", "-v", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
|
||||||
|
|
||||||
convert_mongo_to_pg(
|
|
||||||
args.mongo_uri,
|
|
||||||
args.mongo_db,
|
|
||||||
args.collection,
|
|
||||||
args.pg_dsn,
|
|
||||||
pg_table=args.pg_table,
|
|
||||||
sample_size=args.sample,
|
|
||||||
batch_size=args.batch,
|
|
||||||
create_table=args.create,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
_cli()
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os, time, sys, datetime
|
|
||||||
from db.functions import settype, dbmongo
|
|
||||||
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, desc="Loading series data into MongoDB", unit="series"):
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
actors.append(item['person'])
|
|
||||||
if 'character' in item and item['character']:
|
|
||||||
item['character']['seriesid'] = seriesid
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
import settings.config
|
|
||||||
|
|
||||||
updateList = []
|
|
||||||
updateDict = {}
|
|
||||||
needed_updates = []
|
|
||||||
|
|
||||||
config_options = settings.config.Config(ROOTDIR)
|
|
||||||
options = config_options.config_options
|
|
||||||
mongo_query = dbmongo(options)
|
|
||||||
|
|
||||||
seriesid = 4
|
|
||||||
query = { "id": int(seriesid)}
|
|
||||||
for doc in mongo_query.mgseries.find(query):
|
|
||||||
print(doc)
|
|
||||||
query = { "seriesid": str(seriesid)}
|
|
||||||
for doc in mongo_query.mgepisodes.find(query):
|
|
||||||
print(doc)
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user