"""Module to handle the API routes.""" import os import sqlite3 import json import shutil import math from io import StringIO import tempfile import time from flask import Blueprint, jsonify, request, send_file, session, after_this_request import yaml import mysql.connector from custom_functions.decrypt.api_decrypt import api_decrypt from custom_functions.user_checks.device_allowed import user_allowed_to_use_device from custom_functions.database.unified_db_ops import ( search_by_pssh_or_kid, cache_to_db, get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count, ) from configs.icon_links import data as icon_data api_bp = Blueprint("api", __name__) with open(os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8") as file: config = yaml.safe_load(file) def get_db_config(): """Get the MariaDB database configuration.""" with open( os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8" ) as file_mariadb: config_mariadb = yaml.safe_load(file_mariadb) db_config = { "host": f'{config_mariadb["mariadb"]["host"]}', "user": f'{config_mariadb["mariadb"]["user"]}', "password": f'{config_mariadb["mariadb"]["password"]}', "database": f'{config_mariadb["mariadb"]["database"]}', } return db_config @api_bp.route("/api/cache/search", methods=["POST"]) def get_data(): """Get the data from the database.""" search_argument = json.loads(request.data)["input"] results = search_by_pssh_or_kid(search_filter=search_argument) return jsonify(results) @api_bp.route("/api/cache//", methods=["GET"]) def get_single_key_service(service, kid): """Get the single key from the database.""" result = get_key_by_kid_and_service(kid=kid, service=service) return jsonify( { "code": 0, "content_key": result, } ) @api_bp.route("/api/cache/", methods=["GET"]) def get_multiple_key_service(service): """Get the multiple keys from the database.""" result = get_kid_key_dict(service_name=service) pages = math.ceil(len(result) / 10) return jsonify({"code": 0, "content_keys": result, "pages": pages}) @api_bp.route("/api/cache//", methods=["POST"]) def add_single_key_service(service, kid): """Add the single key to the database.""" body = request.get_json() content_key = body["content_key"] result = cache_to_db(service=service, kid=kid, key=content_key) if result: return jsonify( { "code": 0, "updated": True, } ) return jsonify( { "code": 0, "updated": True, } ) @api_bp.route("/api/cache/", methods=["POST"]) def add_multiple_key_service(service): """Add the multiple keys to the database.""" body = request.get_json() keys_added = 0 keys_updated = 0 for kid, key in body["content_keys"].items(): result = cache_to_db(service=service, kid=kid, key=key) if result is True: keys_updated += 1 else: keys_added += 1 return jsonify( { "code": 0, "added": str(keys_added), "updated": str(keys_updated), } ) @api_bp.route("/api/cache", methods=["POST"]) def unique_service(): """Get the unique services from the database.""" services = get_unique_services() return jsonify( { "code": 0, "service_list": services, } ) @api_bp.route("/api/cache/download", methods=["GET"]) def download_database(): """Download the database.""" if config["database_type"].lower() != "mariadb": original_database_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache.db") # Make a copy of the original database (without locking the original) modified_database_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache_modified.db") # Using shutil.copy2 to preserve metadata (timestamps, etc.) shutil.copy2(original_database_path, modified_database_path) # Open the copied database for modification using 'with' statement to avoid locks with sqlite3.connect(modified_database_path) as conn: cursor = conn.cursor() # Update all rows to remove Headers and Cookies (set them to NULL or empty strings) cursor.execute( """ UPDATE licenses SET Headers = NULL, Cookies = NULL """ ) # No need for explicit commit, it's done automatically with the 'with' block # The connection will automatically be committed and closed when the block ends # Send the modified database as an attachment return send_file( modified_database_path, as_attachment=True, download_name="key_cache.db" ) try: conn = mysql.connector.connect(**get_db_config()) cursor = conn.cursor() # Get column names cursor.execute("SHOW COLUMNS FROM licenses") columns = [row[0] for row in cursor.fetchall()] # Build SELECT with Headers and Cookies as NULL select_columns = [] for col in columns: if col.lower() in ("headers", "cookies"): select_columns.append("NULL AS " + col) else: select_columns.append(col) select_query = f"SELECT {', '.join(select_columns)} FROM licenses" cursor.execute(select_query) rows = cursor.fetchall() # Dump to SQL-like format output = StringIO() output.write("-- Dump of `licenses` table (Headers and Cookies are NULL)\n") for row in rows: values = ", ".join( f"'{str(v).replace('\'', '\\\'')}'" if v is not None else "NULL" for v in row ) output.write( f"INSERT INTO licenses ({', '.join(columns)}) VALUES ({values});\n" ) # Write to a temp file for download temp_dir = tempfile.gettempdir() temp_path = os.path.join(temp_dir, "key_cache.sql") with open(temp_path, "w", encoding="utf-8") as f: f.write(output.getvalue()) @after_this_request def remove_file(response): try: os.remove(temp_path) except Exception: pass return response return send_file( temp_path, as_attachment=True, download_name="licenses_dump.sql" ) except mysql.connector.Error as err: return {"error": str(err)}, 500 _keycount_cache = {"count": None, "timestamp": 0} @api_bp.route("/api/cache/keycount", methods=["GET"]) def get_count(): """Get the count of the keys in the database.""" now = time.time() if now - _keycount_cache["timestamp"] > 10 or _keycount_cache["count"] is None: _keycount_cache["count"] = key_count() _keycount_cache["timestamp"] = now return jsonify({"count": _keycount_cache["count"]}) @api_bp.route("/api/decrypt", methods=["POST"]) def decrypt_data(): """Decrypt the data.""" api_request_data = request.get_json(force=True) # Helper to get fields or None if missing/empty def get_field(key, default=""): value = api_request_data.get(key, default) return value if value != "" else default api_request_pssh = get_field("pssh") api_request_licurl = get_field("licurl") api_request_proxy = get_field("proxy") api_request_headers = get_field("headers") api_request_cookies = get_field("cookies") api_request_data_func = get_field("data") # Device logic device = get_field("device", "public") if device in [ "default", "CDRM-Project Public Widevine CDM", "CDRM-Project Public PlayReady CDM", "", None, ]: api_request_device = "public" else: api_request_device = device username = "" if api_request_device != "public": username = session.get("username") if not username: return jsonify({"message": "Not logged in, not allowed"}), 400 if not user_allowed_to_use_device(device=api_request_device, username=username): return jsonify({"message": "Not authorized / Not found"}), 403 result = api_decrypt( pssh=api_request_pssh, proxy=api_request_proxy, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data_func, device=api_request_device, username=username, ) if result["status"] == "success": return jsonify({"status": "success", "message": result["message"]}) return jsonify({"status": "fail", "message": result["message"]}) @api_bp.route("/api/links", methods=["GET"]) def get_links(): """Get the links.""" return jsonify( { "discord": icon_data["discord"], "telegram": icon_data["telegram"], "gitea": icon_data["gitea"], } ) @api_bp.route("/api/extension", methods=["POST"]) def verify_extension(): """Verify the extension.""" return jsonify( { "status": True, } )