Compare commits
	
		
			3 Commits
		
	
	
		
			aitest
			...
			a68bea95d2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a68bea95d2 | |||
| 7b42d55cc1 | |||
| f325768c1b | 
| @@ -1,2 +1,2 @@ | ||||
| python-qbittorrent==0.4.3 | ||||
| sqlalchemy==2.0.27 | ||||
| qbittorrent-api==2025.2.0 | ||||
| SQLAlchemy==2.0.38 | ||||
|   | ||||
							
								
								
									
										309
									
								
								tarc/main.py
									
									
									
									
									
								
							
							
						
						
									
										309
									
								
								tarc/main.py
									
									
									
									
									
								
							| @@ -15,194 +15,225 @@ import uuid | ||||
| import argparse | ||||
| from datetime import datetime, timezone | ||||
|  | ||||
| import qbittorrent | ||||
| from sqlalchemy import create_engine, event | ||||
| from sqlalchemy.orm import sessionmaker | ||||
| import qbittorrentapi | ||||
| from sqlalchemy import create_engine, inspect | ||||
| from sqlalchemy.orm import Session | ||||
|  | ||||
| from .models import Base, Client | ||||
| from .models import Base, SchemaVersion, Client | ||||
|  | ||||
| # SCHEMA format is YYYYMMDDX | ||||
| SCHEMA = 202410060 | ||||
| SCHEMA = 202503100 | ||||
|  | ||||
|  | ||||
| def init_db(engine): | ||||
|     """ | ||||
|     Initialize database | ||||
|     """ | ||||
|     # Create all tables first | ||||
|     Base.metadata.create_all(engine) | ||||
|      | ||||
|     # Set the schema version using SQLAlchemy primitives | ||||
|     @event.listens_for(engine, 'connect') | ||||
|     def set_sqlite_pragma(dbapi_connection, connection_record): | ||||
|         cursor = dbapi_connection.cursor() | ||||
|         cursor.execute(f"PRAGMA user_version = {SCHEMA}") | ||||
|         cursor.close() | ||||
|  | ||||
|     with Session(engine) as session: | ||||
|         if not session.query(SchemaVersion).first(): | ||||
|             now = datetime.now(timezone.utc) | ||||
|             version = SchemaVersion(version=SCHEMA, applied_at=now) | ||||
|             session.add(version) | ||||
|             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): | ||||
|     """ | ||||
|     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 | ||||
|     """ | ||||
|     new_client = Client( | ||||
|         uuid=str(uuid.uuid4()), | ||||
|         name=name, | ||||
|         endpoint=endpoint, | ||||
|         last_seen=last_seen | ||||
|     ) | ||||
|     session.add(new_client) | ||||
|     session.commit() | ||||
|     with Session(engine) as session: | ||||
|         client = Client( | ||||
|             uuid=str(uuid.uuid4()), name=name, endpoint=endpoint, last_seen=last_seen | ||||
|         ) | ||||
|         session.add(client) | ||||
|         session.commit() | ||||
|  | ||||
|  | ||||
| def find_client(session, endpoint): | ||||
| def find_client(engine, endpoint): | ||||
|     """ | ||||
|     Find existing client | ||||
|     """ | ||||
|     clients = session.query(Client.id, Client.name, Client.uuid).filter(Client.endpoint == endpoint).all() | ||||
|     return clients | ||||
|     with Session(engine) as session: | ||||
|         clients = ( | ||||
|             session.query(Client.id, Client.name, Client.uuid) | ||||
|             .filter_by(endpoint=endpoint) | ||||
|             .all() | ||||
|         ) | ||||
|         return clients | ||||
|  | ||||
|  | ||||
| def list_clients(session): | ||||
| def list_clients(engine): | ||||
|     """ | ||||
|     List all stored clients | ||||
|     """ | ||||
|     return session.query(Client).all() | ||||
|     with Session(engine) as session: | ||||
|         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(): | ||||
|     """ | ||||
|     Entrypoint of the program. | ||||
|     Parses command-line arguments and executes the corresponding command. | ||||
|     """ | ||||
|  | ||||
|     parser = argparse.ArgumentParser(description="Manage BT archives", prog="tarc") | ||||
|     subparsers = parser.add_subparsers( | ||||
|         dest="command", required=True, help="Available commands" | ||||
|     ) | ||||
|     subparsers = parser.add_subparsers(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( | ||||
|         "--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("-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("-u", "--username", help="Username") | ||||
|     scan_parser.add_argument("-p", "--password", help="Password") | ||||
|     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() | ||||
|  | ||||
|     if args.command == "scan": | ||||
|         if args.storage is None: | ||||
|             STORAGE = os.path.expanduser("~/.tarch.db") | ||||
|         else: | ||||
|             STORAGE = args.storage | ||||
|         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) | ||||
|     storage_path = args.storage or os.path.expanduser("~/.tarc.db") | ||||
|     engine = create_engine(f"sqlite:///{storage_path}") | ||||
|  | ||||
|         # Check schema version using SQLAlchemy primitives | ||||
|         schema_found = None | ||||
|          | ||||
|         @event.listens_for(engine, 'connect') | ||||
|         def get_sqlite_pragma(dbapi_connection, connection_record): | ||||
|             nonlocal schema_found | ||||
|             cursor = dbapi_connection.cursor() | ||||
|             cursor.execute("PRAGMA user_version") | ||||
|             schema_found = cursor.fetchone()[0] | ||||
|             cursor.close() | ||||
|          | ||||
|         # Force a connection to trigger the event | ||||
|         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) | ||||
|     if not list_tables(engine): | ||||
|         print(f"[INFO]: Initializing database at {storage_path}") | ||||
|         init_db(engine) | ||||
|  | ||||
|     schema_found = get_schema_version(engine) | ||||
|     if schema_found != SCHEMA: | ||||
|         raise ValueError(f"SCHEMA {schema_found}, expected {SCHEMA}") | ||||
|  | ||||
|     try: | ||||
|         if args.command == "scan": | ||||
|             handle_scan(args, engine) | ||||
|         elif args.command == "client-add": | ||||
|             handle_client_add(args, engine) | ||||
|         elif args.command == "client-list": | ||||
|             handle_client_list(args, engine) | ||||
|     except ValueError as e: | ||||
|         print(f"[ERROR]: {e}") | ||||
|         sys.exit(1) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|   | ||||
							
								
								
									
										141
									
								
								tarc/models.py
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								tarc/models.py
									
									
									
									
									
								
							| @@ -1,109 +1,106 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| Database models for Torrent Archiver | ||||
| """ | ||||
| """SQLAlchemy models for the tarc database.""" | ||||
|  | ||||
| from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, UniqueConstraint | ||||
| from sqlalchemy.ext.declarative import declarative_base | ||||
| from sqlalchemy.orm import relationship | ||||
| from sqlalchemy import ( | ||||
|     Column, | ||||
|     Integer, | ||||
|     String, | ||||
|     DateTime, | ||||
|     Boolean, | ||||
|     ForeignKey, | ||||
|     UniqueConstraint, | ||||
| ) | ||||
| from sqlalchemy.orm import 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) | ||||
|     uuid = Column(String, nullable=False, unique=True) | ||||
|     endpoint = Column(String, 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): | ||||
|     __tablename__ = 'torrents' | ||||
|      | ||||
|     id = Column(Integer, primary_key=True, autoincrement=True) | ||||
| class Torrent(Base):  # pylint: disable=too-few-public-methods | ||||
|     """BitTorrent metadata.""" | ||||
|  | ||||
|     __tablename__ = "torrents" | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     info_hash_v1 = Column(String, nullable=False, unique=True) | ||||
|     info_hash_v2 = Column(String, unique=True) | ||||
|     file_count = Column(Integer, 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): | ||||
|     __tablename__ = 'torrent_clients' | ||||
|      | ||||
|     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) | ||||
| class TorrentClient(Base):  # pylint: disable=too-few-public-methods | ||||
|     """Association between torrents and clients.""" | ||||
|  | ||||
|     __tablename__ = "torrent_clients" | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     torrent_id = Column(Integer, ForeignKey("torrents.id"), nullable=False) | ||||
|     client_id = Column(Integer, ForeignKey("clients.id"), nullable=False) | ||||
|     name = Column(String, nullable=False) | ||||
|     content_path = Column(String, nullable=False) | ||||
|     last_seen = Column(DateTime, nullable=False) | ||||
|      | ||||
|     torrent = relationship("Torrent", back_populates="torrent_clients") | ||||
|     client = relationship("Client", back_populates="torrent_clients") | ||||
|      | ||||
|     __table_args__ = (UniqueConstraint('torrent_id', 'client_id'),) | ||||
|     __table_args__ = (UniqueConstraint("torrent_id", "client_id"),) | ||||
|  | ||||
|  | ||||
| class Tracker(Base): | ||||
|     __tablename__ = 'trackers' | ||||
|      | ||||
|     id = Column(Integer, primary_key=True, autoincrement=True) | ||||
| class Tracker(Base):  # pylint: disable=too-few-public-methods | ||||
|     """BitTorrent tracker information.""" | ||||
|  | ||||
|     __tablename__ = "trackers" | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     url = Column(String, nullable=False, unique=True) | ||||
|     last_seen = Column(DateTime, nullable=False) | ||||
|      | ||||
|     torrent_trackers = relationship("TorrentTracker", back_populates="tracker") | ||||
|  | ||||
|  | ||||
| class TorrentTracker(Base): | ||||
|     __tablename__ = 'torrent_trackers' | ||||
|      | ||||
|     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) | ||||
|     tracker_id = Column(Integer, ForeignKey('trackers.id'), nullable=False) | ||||
| class TorrentTracker(Base):  # pylint: disable=too-few-public-methods | ||||
|     """Association between torrents and trackers.""" | ||||
|  | ||||
|     __tablename__ = "torrent_trackers" | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     client_id = Column(Integer, ForeignKey("clients.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) | ||||
|      | ||||
|     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'),) | ||||
|     __table_args__ = (UniqueConstraint("client_id", "torrent_id", "tracker_id"),) | ||||
|  | ||||
|  | ||||
| class File(Base): | ||||
|     __tablename__ = 'files' | ||||
|      | ||||
|     id = Column(Integer, primary_key=True, autoincrement=True) | ||||
| class File(Base):  # pylint: disable=too-few-public-methods | ||||
|     """File metadata and hashes.""" | ||||
|  | ||||
|     __tablename__ = "files" | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     size = Column(Integer, nullable=False) | ||||
|     oshash = Column(String, nullable=False, unique=True) | ||||
|     hash = Column(String, unique=True) | ||||
|      | ||||
|     torrent_files = relationship("TorrentFile", back_populates="file") | ||||
|  | ||||
|  | ||||
| class TorrentFile(Base): | ||||
|     __tablename__ = 'torrent_files' | ||||
|      | ||||
|     id = Column(Integer, primary_key=True, autoincrement=True) | ||||
|     file_id = Column(Integer, ForeignKey('files.id'), nullable=False) | ||||
|     torrent_id = Column(Integer, ForeignKey('torrents.id'), nullable=False) | ||||
|     client_id = Column(Integer, ForeignKey('clients.id'), nullable=False) | ||||
| class TorrentFile(Base):  # pylint: disable=too-few-public-methods | ||||
|     """Association between torrents and files.""" | ||||
|  | ||||
|     __tablename__ = "torrent_files" | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     file_id = Column(Integer, ForeignKey("files.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_path = Column(String, nullable=False) | ||||
|     is_downloaded = Column(Boolean, nullable=False) | ||||
|     last_checked = Column(DateTime, nullable=False) | ||||
|      | ||||
|     file = relationship("File", back_populates="torrent_files") | ||||
|     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'),)  | ||||
|     __table_args__ = ( | ||||
|         UniqueConstraint("file_id", "torrent_id", "client_id", "file_index"), | ||||
|     ) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user