Compare commits
	
		
			3 Commits
		
	
	
		
			aitest
			...
			a68bea95d2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a68bea95d2 | |||
| 7b42d55cc1 | |||
| f325768c1b | 
| @@ -1,2 +1,2 @@ | |||||||
| python-qbittorrent==0.4.3 | qbittorrent-api==2025.2.0 | ||||||
| sqlalchemy==2.0.27 | SQLAlchemy==2.0.38 | ||||||
|   | |||||||
							
								
								
									
										289
									
								
								tarc/main.py
									
									
									
									
									
								
							
							
						
						
									
										289
									
								
								tarc/main.py
									
									
									
									
									
								
							| @@ -15,193 +15,224 @@ import uuid | |||||||
| import argparse | import argparse | ||||||
| from datetime import datetime, timezone | from datetime import datetime, timezone | ||||||
|  |  | ||||||
| import qbittorrent | import qbittorrentapi | ||||||
| from sqlalchemy import create_engine, event | from sqlalchemy import create_engine, inspect | ||||||
| from sqlalchemy.orm import sessionmaker | from sqlalchemy.orm import Session | ||||||
|  |  | ||||||
| from .models import Base, Client | from .models import Base, SchemaVersion, Client | ||||||
|  |  | ||||||
| # SCHEMA format is YYYYMMDDX | # SCHEMA format is YYYYMMDDX | ||||||
| SCHEMA = 202410060 | SCHEMA = 202503100 | ||||||
|  |  | ||||||
|  |  | ||||||
| def init_db(engine): | def init_db(engine): | ||||||
|     """ |     """ | ||||||
|     Initialize database |     Initialize database | ||||||
|     """ |     """ | ||||||
|     # Create all tables first |  | ||||||
|     Base.metadata.create_all(engine) |     Base.metadata.create_all(engine) | ||||||
|  |  | ||||||
|     # Set the schema version using SQLAlchemy primitives |     with Session(engine) as session: | ||||||
|     @event.listens_for(engine, 'connect') |         if not session.query(SchemaVersion).first(): | ||||||
|     def set_sqlite_pragma(dbapi_connection, connection_record): |             now = datetime.now(timezone.utc) | ||||||
|         cursor = dbapi_connection.cursor() |             version = SchemaVersion(version=SCHEMA, applied_at=now) | ||||||
|         cursor.execute(f"PRAGMA user_version = {SCHEMA}") |             session.add(version) | ||||||
|         cursor.close() |             session.commit() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_schema_version(engine): | ||||||
|  |     """ | ||||||
|  |     Get current schema version from database | ||||||
|  |     """ | ||||||
|  |     with Session(engine) as session: | ||||||
|  |         version = session.query(SchemaVersion).order_by(SchemaVersion.id.desc()).first() | ||||||
|  |         return version.version if version else None | ||||||
|  |  | ||||||
|  |  | ||||||
| def list_tables(engine): | def list_tables(engine): | ||||||
|     """ |     """ | ||||||
|     List all tables in database |     List all tables in database | ||||||
|     """ |     """ | ||||||
|     return Base.metadata.tables.keys() |     inspector = inspect(engine) | ||||||
|  |     return inspector.get_table_names() | ||||||
|  |  | ||||||
|  |  | ||||||
| def add_client(session, name, endpoint, last_seen): | def add_client(engine, name, endpoint, last_seen): | ||||||
|     """ |     """ | ||||||
|     Add a new client endpoint to database |     Add a new client endpoint to database | ||||||
|     """ |     """ | ||||||
|     new_client = Client( |     with Session(engine) as session: | ||||||
|         uuid=str(uuid.uuid4()), |         client = Client( | ||||||
|         name=name, |             uuid=str(uuid.uuid4()), name=name, endpoint=endpoint, last_seen=last_seen | ||||||
|         endpoint=endpoint, |  | ||||||
|         last_seen=last_seen |  | ||||||
|         ) |         ) | ||||||
|     session.add(new_client) |         session.add(client) | ||||||
|         session.commit() |         session.commit() | ||||||
|  |  | ||||||
|  |  | ||||||
| def find_client(session, endpoint): | def find_client(engine, endpoint): | ||||||
|     """ |     """ | ||||||
|     Find existing client |     Find existing client | ||||||
|     """ |     """ | ||||||
|     clients = session.query(Client.id, Client.name, Client.uuid).filter(Client.endpoint == endpoint).all() |     with Session(engine) as session: | ||||||
|  |         clients = ( | ||||||
|  |             session.query(Client.id, Client.name, Client.uuid) | ||||||
|  |             .filter_by(endpoint=endpoint) | ||||||
|  |             .all() | ||||||
|  |         ) | ||||||
|         return clients |         return clients | ||||||
|  |  | ||||||
|  |  | ||||||
| def list_clients(session): | def list_clients(engine): | ||||||
|     """ |     """ | ||||||
|     List all stored clients |     List all stored clients | ||||||
|     """ |     """ | ||||||
|  |     with Session(engine) as session: | ||||||
|         return session.query(Client).all() |         return session.query(Client).all() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def authenticate_qbittorrent(endpoint, username, password): | ||||||
|  |     """ | ||||||
|  |     Authenticate with the qBittorrent client | ||||||
|  |     """ | ||||||
|  |     qb = qbittorrentapi.Client(host=endpoint, username=username, password=password) | ||||||
|  |     try: | ||||||
|  |         qb.auth_log_in() | ||||||
|  |     except qbittorrentapi.LoginFailed as e: | ||||||
|  |         raise ValueError(f'Login failed for endpoint "{endpoint}": {e}') from e | ||||||
|  |     if not re.match(r"^v?\d+(\.\d+)*$", qb.app.version): | ||||||
|  |         raise ValueError(f'Invalid version "{qb.app.version}" found at "{endpoint}"') | ||||||
|  |     return qb | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def scan_torrents(qb_client, debug=False): | ||||||
|  |     """ | ||||||
|  |     Scan torrents using the provided qBittorrent client. | ||||||
|  |     """ | ||||||
|  |     torrents = qb_client.torrents_info() | ||||||
|  |     print(f"[INFO]: There are {len(torrents)} torrents\n") | ||||||
|  |     for torrent in torrents[:2]: | ||||||
|  |         files = qb_client.torrents_files(torrent.hash) | ||||||
|  |         trackers = qb_client.torrents_trackers(torrent.hash) | ||||||
|  |         print(f"[name]: {torrent.name}") | ||||||
|  |         print(f"[infohash_v1]: {torrent.hash}") | ||||||
|  |         print(f"[content_path]: {torrent.content_path}") | ||||||
|  |         print(f"[magnet_uri]: {torrent.magnet_uri[:80]}") | ||||||
|  |         print(f"[completed_on]: {torrent.completed}\n") | ||||||
|  |         print(f"[trackers]: {len(trackers)}") | ||||||
|  |         print(f"[file_count]: {len(files)}\n") | ||||||
|  |         if debug: | ||||||
|  |             print(f"[DEBUG]: {repr(torrent)}") | ||||||
|  |             for elem in trackers: | ||||||
|  |                 print(f"[DEBUG]: Tracker {repr(elem)}") | ||||||
|  |             print("\n", end="") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def handle_scan(args, engine): | ||||||
|  |     """ | ||||||
|  |     Handle the scan command to authenticate with the qBittorrent client and scan torrents. | ||||||
|  |     """ | ||||||
|  |     if args.endpoint: | ||||||
|  |         qb_client = authenticate_qbittorrent(args.endpoint, args.username, args.password) | ||||||
|  |         clients = find_client(engine, args.endpoint) | ||||||
|  |         if args.confirm_add: | ||||||
|  |             if len(clients) == 0: | ||||||
|  |                 if args.name: | ||||||
|  |                     now = datetime.now(timezone.utc) | ||||||
|  |                     add_client(engine, args.name, args.endpoint, now) | ||||||
|  |                     print(f"[INFO]: Added client {args.name} ({args.endpoint})") | ||||||
|  |                 else: | ||||||
|  |                     raise ValueError("Must specify --name for a new client") | ||||||
|  |             elif len(clients) == 1: | ||||||
|  |                 raise ValueError(f"{clients[0][1]} ({clients[0][2]}) already exists") | ||||||
|  |             else: | ||||||
|  |                 raise ValueError(f"Multiple clients with the same endpoint: {args.endpoint}") | ||||||
|  |         elif len(clients) == 0: | ||||||
|  |             raise ValueError( | ||||||
|  |                 f'Client using endpoint "{args.endpoint}" not found. ' | ||||||
|  |                 'Use --confirm-add to add a new endpoint' | ||||||
|  |             ) | ||||||
|  |         elif len(clients) == 1: | ||||||
|  |             scan_torrents(qb_client, debug=args.debug) | ||||||
|  |         else: | ||||||
|  |             raise ValueError(f'Multiple clients ({len(clients)}) using "{args.endpoint}"') | ||||||
|  |     elif args.directory: | ||||||
|  |         print("[INFO]: --directory is not implemented") | ||||||
|  |     else: | ||||||
|  |         raise ValueError("Must specify directory OR client endpoint") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def handle_client_add(args, engine): | ||||||
|  |     """ | ||||||
|  |     Handle the client addition command to add a new client to the database. | ||||||
|  |     """ | ||||||
|  |     if args.name and args.endpoint: | ||||||
|  |         now = datetime.now(timezone.utc) | ||||||
|  |         add_client(engine, args.name, args.endpoint, now) | ||||||
|  |         print(f"[INFO]: Added client {args.name} ({args.endpoint})") | ||||||
|  |     else: | ||||||
|  |         raise ValueError("Must specify --name and --endpoint to add a client") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def handle_client_list(args, engine): | ||||||
|  |     """ | ||||||
|  |     Handle the client listing command to display all stored clients. | ||||||
|  |     """ | ||||||
|  |     clients = list_clients(engine) | ||||||
|  |     for client in clients: | ||||||
|  |         print(f"{client.name} ({client.endpoint}) - Last seen: {client.last_seen}") | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     """ |     """ | ||||||
|     Entrypoint of the program. |     Parses command-line arguments and executes the corresponding command. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     parser = argparse.ArgumentParser(description="Manage BT archives", prog="tarc") |     parser = argparse.ArgumentParser(description="Manage BT archives", prog="tarc") | ||||||
|     subparsers = parser.add_subparsers( |     subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands") | ||||||
|         dest="command", required=True, help="Available commands" |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     scan_parser = subparsers.add_parser("scan", help="Scan command") |     # scan command | ||||||
|  |     scan_parser = subparsers.add_parser("scan", help="Scan torrents") | ||||||
|     scan_parser.add_argument("--debug", action="store_true", help="Enable debug mode") |     scan_parser.add_argument("--debug", action="store_true", help="Enable debug mode") | ||||||
|     scan_parser.add_argument( |     scan_parser.add_argument( | ||||||
|             "--confirm-add", action="store_true", help="Confirm adding a new client" |             "--confirm-add", action="store_true", help="Confirm adding a new client" | ||||||
|         ) |         ) | ||||||
|     scan_parser.add_argument("-n", "--name", help="Name of client") |     scan_parser.add_argument("-n", "--name", help="Name of client") | ||||||
|     scan_parser.add_argument("-d", "--directory", help="Directory to scan") |     scan_parser.add_argument("-d", "--directory", help="Directory to scan") | ||||||
|     scan_parser.add_argument("-t", "--type", help="Scan type") |  | ||||||
|     scan_parser.add_argument("-e", "--endpoint", help="Endpoint URL") |     scan_parser.add_argument("-e", "--endpoint", help="Endpoint URL") | ||||||
|     scan_parser.add_argument("-u", "--username", help="Username") |     scan_parser.add_argument("-u", "--username", help="Username") | ||||||
|     scan_parser.add_argument("-p", "--password", help="Password") |     scan_parser.add_argument("-p", "--password", help="Password") | ||||||
|     scan_parser.add_argument("-s", "--storage", help="Path of sqlite3 database") |     scan_parser.add_argument("-s", "--storage", help="Path of sqlite3 database") | ||||||
|  |  | ||||||
|  |     # client add command | ||||||
|  |     client_add_parser = subparsers.add_parser("client-add", help="Add a new client") | ||||||
|  |     client_add_parser.add_argument("-n", "--name", required=True, help="Name of client") | ||||||
|  |     client_add_parser.add_argument("-e", "--endpoint", required=True, help="Endpoint URL") | ||||||
|  |     client_add_parser.add_argument("-s", "--storage", help="Path of sqlite3 database") | ||||||
|  |  | ||||||
|  |     # client list command | ||||||
|  |     client_list_parser = subparsers.add_parser("client-list", help="List all clients") | ||||||
|  |     client_list_parser.add_argument("-s", "--storage", help="Path of sqlite3 database") | ||||||
|  |  | ||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
|  |  | ||||||
|     if args.command == "scan": |     storage_path = args.storage or os.path.expanduser("~/.tarc.db") | ||||||
|         if args.storage is None: |     engine = create_engine(f"sqlite:///{storage_path}") | ||||||
|             STORAGE = os.path.expanduser("~/.tarch.db") |  | ||||||
|         else: |     if not list_tables(engine): | ||||||
|             STORAGE = args.storage |         print(f"[INFO]: Initializing database at {storage_path}") | ||||||
|         try: |  | ||||||
|             engine = create_engine(f"sqlite:///{STORAGE}") |  | ||||||
|             tables = list_tables(engine) |  | ||||||
|             Session = sessionmaker(bind=engine) |  | ||||||
|             session = Session() |  | ||||||
|         except Exception as e: |  | ||||||
|             print(f'[ERROR]: Database Error "{STORAGE}" ({str(e)})') |  | ||||||
|             sys.exit(1) |  | ||||||
|         if len(tables) == 0: |  | ||||||
|             print(f"[INFO]: Initializing database at {STORAGE}") |  | ||||||
|         init_db(engine) |         init_db(engine) | ||||||
|  |  | ||||||
|         # Check schema version using SQLAlchemy primitives |     schema_found = get_schema_version(engine) | ||||||
|         schema_found = None |     if schema_found != SCHEMA: | ||||||
|  |         raise ValueError(f"SCHEMA {schema_found}, expected {SCHEMA}") | ||||||
|  |  | ||||||
|         @event.listens_for(engine, 'connect') |     try: | ||||||
|         def get_sqlite_pragma(dbapi_connection, connection_record): |         if args.command == "scan": | ||||||
|             nonlocal schema_found |             handle_scan(args, engine) | ||||||
|             cursor = dbapi_connection.cursor() |         elif args.command == "client-add": | ||||||
|             cursor.execute("PRAGMA user_version") |             handle_client_add(args, engine) | ||||||
|             schema_found = cursor.fetchone()[0] |         elif args.command == "client-list": | ||||||
|             cursor.close() |             handle_client_list(args, engine) | ||||||
|          |     except ValueError as e: | ||||||
|         # Force a connection to trigger the event |         print(f"[ERROR]: {e}") | ||||||
|         with engine.connect() as conn: |  | ||||||
|             pass |  | ||||||
|          |  | ||||||
|         if not SCHEMA == schema_found: |  | ||||||
|             print(f"[ERROR]: SCHEMA {schema_found}, expected {SCHEMA}") |  | ||||||
|             sys.exit(1) |  | ||||||
|         if not args.directory is None: |  | ||||||
|             print("[INFO]: --directory is not implemented") |  | ||||||
|             sys.exit(0) |  | ||||||
|         elif not args.endpoint is None: |  | ||||||
|             qb = qbittorrent.Client(args.endpoint) |  | ||||||
|             if args.username and args.password: |  | ||||||
|                 qb.login(args.username, args.password) |  | ||||||
|             if qb.qbittorrent_version is None: |  | ||||||
|                 print(f'[ERROR]: Couldn\'t find client version at "{args.endpoint}"') |  | ||||||
|                 sys.exit(1) |  | ||||||
|             elif not re.match(r"^v?\d+(\.\d+)*$", qb.qbittorrent_version): |  | ||||||
|                 print(f'[ERROR]: Invalid version found at "{args.endpoint}"') |  | ||||||
|                 if args.debug: |  | ||||||
|                     print(f"[DEBUG]: {qb.qbittorrent_version}") |  | ||||||
|                 sys.exit(1) |  | ||||||
|             else: |  | ||||||
|                 print( |  | ||||||
|                     f'[INFO]: Found qbittorrent {qb.qbittorrent_version} at "{args.endpoint}"' |  | ||||||
|                 ) |  | ||||||
|             clients = find_client(session, args.endpoint) |  | ||||||
|             if args.confirm_add: |  | ||||||
|                 if len(clients) == 0: |  | ||||||
|                     if not args.name is None: |  | ||||||
|                         now = datetime.now(timezone.utc) |  | ||||||
|                         add_client(session, args.name, args.endpoint, now) |  | ||||||
|                         print(f"[INFO]: Added client {args.name} ({args.endpoint})") |  | ||||||
|                     else: |  | ||||||
|                         print("[ERROR]: Must specify --name for a new client") |  | ||||||
|                         sys.exit(1) |  | ||||||
|                 elif len(clients) == 1: |  | ||||||
|                     print(f"[ERROR]: {clients[0][1]} ({clients[0][2]}) already exists") |  | ||||||
|                     sys.exit(1) |  | ||||||
|                 else: |  | ||||||
|                     print( |  | ||||||
|                         f"[ERROR]: Multiple clients with the same endpoint: {args.endpoint}" |  | ||||||
|                     ) |  | ||||||
|                     sys.exit(1) |  | ||||||
|             elif len(clients) == 0: |  | ||||||
|                 print(f'[ERROR]: Client using endpoint "{args.endpoint}" not found') |  | ||||||
|                 print("[ERROR]: Use --confirm-add to add a new endpoint") |  | ||||||
|                 sys.exit(1) |  | ||||||
|             elif len(clients) == 1: |  | ||||||
|                 torrents = qb.torrents() |  | ||||||
|                 print(f"[INFO]: There are {len(torrents)} torrents\n") |  | ||||||
|                  |  | ||||||
|                 for torrent in torrents[:2]: |  | ||||||
|                     files = qb.get_torrent_files(torrent["hash"]) |  | ||||||
|                     trackers = qb.get_torrent_trackers(torrent["hash"]) |  | ||||||
|                     print(f"[name]: {torrent['name']}") |  | ||||||
|                     print(f"[infohash_v1]: {torrent['infohash_v1']}") |  | ||||||
|                     print(f"[content_path]: {torrent['content_path']}") |  | ||||||
|                     print(f"[magent_uri]: {torrent['magnet_uri'][0:80]}") |  | ||||||
|                     print(f"[completed_on]: {torrent['completed']}") |  | ||||||
|                     print(f"[trackers]: {len(trackers)}") |  | ||||||
|                     print(f"[file_count]: {len(files)}\n") |  | ||||||
|                     if args.debug: |  | ||||||
|                         print(f"[DEBUG]: {repr(torrent)}") |  | ||||||
|                         for elem in trackers: |  | ||||||
|                             print(f"[DEBUG]: Tracker {repr(elem)}") |  | ||||||
|                         print("\n", end="") |  | ||||||
|             else: |  | ||||||
|                 print( |  | ||||||
|                     f'[ERROR]: Multiple clients ({len(clients)}) using "{args.endpoint}"' |  | ||||||
|                 ) |  | ||||||
|                 sys.exit(1) |  | ||||||
|         else: |  | ||||||
|             print("[ERROR]: Must specify directory OR client endpoint") |  | ||||||
|         sys.exit(1) |         sys.exit(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										127
									
								
								tarc/models.py
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								tarc/models.py
									
									
									
									
									
								
							| @@ -1,109 +1,106 @@ | |||||||
| #!/usr/bin/env python3 | """SQLAlchemy models for the tarc database.""" | ||||||
| """ |  | ||||||
| Database models for Torrent Archiver |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, UniqueConstraint | from sqlalchemy import ( | ||||||
| from sqlalchemy.ext.declarative import declarative_base |     Column, | ||||||
| from sqlalchemy.orm import relationship |     Integer, | ||||||
|  |     String, | ||||||
|  |     DateTime, | ||||||
|  |     Boolean, | ||||||
|  |     ForeignKey, | ||||||
|  |     UniqueConstraint, | ||||||
|  | ) | ||||||
|  | from sqlalchemy.orm import declarative_base | ||||||
|  |  | ||||||
| Base = declarative_base() | Base = declarative_base() | ||||||
|  |  | ||||||
| class Client(Base): |  | ||||||
|     __tablename__ = 'clients' |  | ||||||
|  |  | ||||||
|     id = Column(Integer, primary_key=True, autoincrement=True) | class SchemaVersion(Base):  # pylint: disable=too-few-public-methods | ||||||
|  |     """Database schema version tracking.""" | ||||||
|  |  | ||||||
|  |     __tablename__ = "schema_version" | ||||||
|  |     id = Column(Integer, primary_key=True) | ||||||
|  |     version = Column(Integer, nullable=False) | ||||||
|  |     applied_at = Column(DateTime, nullable=False) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Client(Base):  # pylint: disable=too-few-public-methods | ||||||
|  |     """BitTorrent client instance.""" | ||||||
|  |  | ||||||
|  |     __tablename__ = "clients" | ||||||
|  |     id = Column(Integer, primary_key=True) | ||||||
|     name = Column(String, nullable=False, unique=True) |     name = Column(String, nullable=False, unique=True) | ||||||
|     uuid = Column(String, nullable=False, unique=True) |     uuid = Column(String, nullable=False, unique=True) | ||||||
|     endpoint = Column(String, nullable=False) |     endpoint = Column(String, nullable=False) | ||||||
|     last_seen = Column(DateTime, nullable=False) |     last_seen = Column(DateTime, nullable=False) | ||||||
|  |  | ||||||
|     torrent_clients = relationship("TorrentClient", back_populates="client") |  | ||||||
|     torrent_trackers = relationship("TorrentTracker", back_populates="client") |  | ||||||
|     torrent_files = relationship("TorrentFile", back_populates="client") |  | ||||||
|  |  | ||||||
|  | class Torrent(Base):  # pylint: disable=too-few-public-methods | ||||||
|  |     """BitTorrent metadata.""" | ||||||
|  |  | ||||||
| class Torrent(Base): |     __tablename__ = "torrents" | ||||||
|     __tablename__ = 'torrents' |     id = Column(Integer, primary_key=True) | ||||||
|      |  | ||||||
|     id = Column(Integer, primary_key=True, autoincrement=True) |  | ||||||
|     info_hash_v1 = Column(String, nullable=False, unique=True) |     info_hash_v1 = Column(String, nullable=False, unique=True) | ||||||
|     info_hash_v2 = Column(String, unique=True) |     info_hash_v2 = Column(String, unique=True) | ||||||
|     file_count = Column(Integer, nullable=False) |     file_count = Column(Integer, nullable=False) | ||||||
|     completed_on = Column(DateTime, nullable=False) |     completed_on = Column(DateTime, nullable=False) | ||||||
|  |  | ||||||
|     torrent_clients = relationship("TorrentClient", back_populates="torrent") |  | ||||||
|     torrent_trackers = relationship("TorrentTracker", back_populates="torrent") |  | ||||||
|     torrent_files = relationship("TorrentFile", back_populates="torrent") |  | ||||||
|  |  | ||||||
|  | class TorrentClient(Base):  # pylint: disable=too-few-public-methods | ||||||
|  |     """Association between torrents and clients.""" | ||||||
|  |  | ||||||
| class TorrentClient(Base): |     __tablename__ = "torrent_clients" | ||||||
|     __tablename__ = 'torrent_clients' |     id = Column(Integer, primary_key=True) | ||||||
|      |     torrent_id = Column(Integer, ForeignKey("torrents.id"), nullable=False) | ||||||
|     id = Column(Integer, primary_key=True, autoincrement=True) |     client_id = Column(Integer, ForeignKey("clients.id"), nullable=False) | ||||||
|     torrent_id = Column(Integer, ForeignKey('torrents.id'), nullable=False) |  | ||||||
|     client_id = Column(Integer, ForeignKey('clients.id'), nullable=False) |  | ||||||
|     name = Column(String, nullable=False) |     name = Column(String, nullable=False) | ||||||
|     content_path = Column(String, nullable=False) |     content_path = Column(String, nullable=False) | ||||||
|     last_seen = Column(DateTime, nullable=False) |     last_seen = Column(DateTime, nullable=False) | ||||||
|      |     __table_args__ = (UniqueConstraint("torrent_id", "client_id"),) | ||||||
|     torrent = relationship("Torrent", back_populates="torrent_clients") |  | ||||||
|     client = relationship("Client", back_populates="torrent_clients") |  | ||||||
|      |  | ||||||
|     __table_args__ = (UniqueConstraint('torrent_id', 'client_id'),) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Tracker(Base): | class Tracker(Base):  # pylint: disable=too-few-public-methods | ||||||
|     __tablename__ = 'trackers' |     """BitTorrent tracker information.""" | ||||||
|  |  | ||||||
|     id = Column(Integer, primary_key=True, autoincrement=True) |     __tablename__ = "trackers" | ||||||
|  |     id = Column(Integer, primary_key=True) | ||||||
|     url = Column(String, nullable=False, unique=True) |     url = Column(String, nullable=False, unique=True) | ||||||
|     last_seen = Column(DateTime, nullable=False) |     last_seen = Column(DateTime, nullable=False) | ||||||
|  |  | ||||||
|     torrent_trackers = relationship("TorrentTracker", back_populates="tracker") |  | ||||||
|  |  | ||||||
|  | class TorrentTracker(Base):  # pylint: disable=too-few-public-methods | ||||||
|  |     """Association between torrents and trackers.""" | ||||||
|  |  | ||||||
| class TorrentTracker(Base): |     __tablename__ = "torrent_trackers" | ||||||
|     __tablename__ = 'torrent_trackers' |     id = Column(Integer, primary_key=True) | ||||||
|      |     client_id = Column(Integer, ForeignKey("clients.id"), nullable=False) | ||||||
|     id = Column(Integer, primary_key=True, autoincrement=True) |     torrent_id = Column(Integer, ForeignKey("torrents.id"), nullable=False) | ||||||
|     client_id = Column(Integer, ForeignKey('clients.id'), nullable=False) |     tracker_id = Column(Integer, ForeignKey("trackers.id"), nullable=False) | ||||||
|     torrent_id = Column(Integer, ForeignKey('torrents.id'), nullable=False) |  | ||||||
|     tracker_id = Column(Integer, ForeignKey('trackers.id'), nullable=False) |  | ||||||
|     last_seen = Column(DateTime, nullable=False) |     last_seen = Column(DateTime, nullable=False) | ||||||
|      |     __table_args__ = (UniqueConstraint("client_id", "torrent_id", "tracker_id"),) | ||||||
|     client = relationship("Client", back_populates="torrent_trackers") |  | ||||||
|     torrent = relationship("Torrent", back_populates="torrent_trackers") |  | ||||||
|     tracker = relationship("Tracker", back_populates="torrent_trackers") |  | ||||||
|      |  | ||||||
|     __table_args__ = (UniqueConstraint('client_id', 'torrent_id', 'tracker_id'),) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class File(Base): | class File(Base):  # pylint: disable=too-few-public-methods | ||||||
|     __tablename__ = 'files' |     """File metadata and hashes.""" | ||||||
|  |  | ||||||
|     id = Column(Integer, primary_key=True, autoincrement=True) |     __tablename__ = "files" | ||||||
|  |     id = Column(Integer, primary_key=True) | ||||||
|     size = Column(Integer, nullable=False) |     size = Column(Integer, nullable=False) | ||||||
|     oshash = Column(String, nullable=False, unique=True) |     oshash = Column(String, nullable=False, unique=True) | ||||||
|     hash = Column(String, unique=True) |     hash = Column(String, unique=True) | ||||||
|  |  | ||||||
|     torrent_files = relationship("TorrentFile", back_populates="file") |  | ||||||
|  |  | ||||||
|  | class TorrentFile(Base):  # pylint: disable=too-few-public-methods | ||||||
|  |     """Association between torrents and files.""" | ||||||
|  |  | ||||||
| class TorrentFile(Base): |     __tablename__ = "torrent_files" | ||||||
|     __tablename__ = 'torrent_files' |     id = Column(Integer, primary_key=True) | ||||||
|      |     file_id = Column(Integer, ForeignKey("files.id"), nullable=False) | ||||||
|     id = Column(Integer, primary_key=True, autoincrement=True) |     torrent_id = Column(Integer, ForeignKey("torrents.id"), nullable=False) | ||||||
|     file_id = Column(Integer, ForeignKey('files.id'), nullable=False) |     client_id = Column(Integer, ForeignKey("clients.id"), nullable=False) | ||||||
|     torrent_id = Column(Integer, ForeignKey('torrents.id'), nullable=False) |  | ||||||
|     client_id = Column(Integer, ForeignKey('clients.id'), nullable=False) |  | ||||||
|     file_index = Column(Integer, nullable=False) |     file_index = Column(Integer, nullable=False) | ||||||
|     file_path = Column(String, nullable=False) |     file_path = Column(String, nullable=False) | ||||||
|     is_downloaded = Column(Boolean, nullable=False) |     is_downloaded = Column(Boolean, nullable=False) | ||||||
|     last_checked = Column(DateTime, nullable=False) |     last_checked = Column(DateTime, nullable=False) | ||||||
|      |     __table_args__ = ( | ||||||
|     file = relationship("File", back_populates="torrent_files") |         UniqueConstraint("file_id", "torrent_id", "client_id", "file_index"), | ||||||
|     torrent = relationship("Torrent", back_populates="torrent_files") |     ) | ||||||
|     client = relationship("Client", back_populates="torrent_files") |  | ||||||
|      |  | ||||||
|     __table_args__ = (UniqueConstraint('file_id', 'torrent_id', 'client_id', 'file_index'),)  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user