Skip to main content
The Cricfy Kodi Plugin is built with a modular architecture. Each module in the lib/ directory serves a specific purpose, making the codebase maintainable and extensible.

Core modules

The configuration module initializes the addon environment and caching layer.

Purpose

Provides centralized access to addon paths and the caching mechanism used throughout the plugin.

Key components

ADDON_PATH from config.py:9
ADDON_PATH = Path(translatePath(Addon().getAddonInfo('path')))
Resolves the addon’s installation directory as a Path object for easy file access.cache from config.py:10
cache = StorageServer.StorageServer("cricfy_plugin", 24)
Initializes the StorageServer cache with:
  • Table name: "cricfy_plugin"
  • Default TTL: 24 hours

Usage

Imported by other modules to access addon resources and cache data:
from lib.config import cache, ADDON_PATH

# Read addon resources
secret_path = ADDON_PATH / "resources" / "secret1.txt"

# Cache data
cache.set("key", "value")
value = cache.get("key")
The providers module handles fetching, caching, and managing provider and channel data.

Purpose

Provides high-level functions to retrieve provider lists and channel playlists with automatic caching and decryption.

Key functions

get_providers() from providers.py:22-59
def get_providers():
  """
  Fetches and decrypts the list of providers from Cricfy.
  Uses caching to avoid repeated network calls.
  """
  cached_providers = cache.get(PROVIDERS_CACHE_KEY)
  if cached_providers and isinstance(cached_providers, str):
    return json.loads(cached_providers)

  url = get_provider_api_url()
  response = fetch_url(f"{url}/cats.txt", timeout=15)
  decrypted_data = decrypt_data(response)
  providers = json.loads(decrypted_data)
  
  cache.set(PROVIDERS_CACHE_KEY, decrypted_data)
  return providers
Retrieves the list of available providers. Checks cache first, then fetches from the remote API if needed.get_channels(provider_url) from providers.py:62-90
def get_channels(provider_url: str):
  """
  Fetches channels for a specific provider.
  """
  channel_cache_key = f"channels_{_hash_key(provider_url)}"
  cached_channels = cache.get(channel_cache_key)
  
  # Check cache with TTL validation
  if cached_channels:
    channel_data = json.loads(cached_channels)
    fetch_time = float(channel_data.get('fetch_time'))
    if time.time() - fetch_time <= CHANNEL_CACHE_TTL:
      return [PlaylistItem.from_dict(item) for item in channels]
  
  # Fetch and parse M3U
  content = fetch_url(provider_url, timeout=15)
  content = decrypt_content(content)
  channels = parse_m3u(content)
  
  # Cache with timestamp
  cache.set(channel_cache_key, json.dumps({
    'channels': json.dumps(channels, default=lambda o: o.to_dict()),
    'fetch_time': time.time()
  }))
  return channels
Fetches and parses channels from a provider’s M3U playlist URL. Implements time-based cache validation.

Caching constants

  • PROVIDERS_CACHE_KEY = "cricfy_providers" from providers.py:11
  • CHANNEL_CACHE_TTL = 3600 (1 hour) from providers.py:12

Helper functions

_hash_key(key) from providers.py:15-19Generates SHA256 hash of provider URLs for cache key generation.
The M3U parser extracts channel information from M3U8 playlists.

Purpose

Parses M3U playlist content and converts it into structured PlaylistItem objects containing all metadata needed for playback.

PlaylistItem class

From m3u_parser.py:5-32:
class PlaylistItem:
  def __init__(self):
    self.title = ""
    self.url = ""
    self.tvg_logo = ""
    self.group_title = ""
    self.user_agent = ""
    self.cookie = ""
    self.referer = ""
    self.license_string = ""
    self.headers = {}
    self.is_drm = False
Represents a single channel with all its playback metadata.Methods:
  • to_json() - Serializes to JSON string
  • to_dict() - Converts to dictionary
  • from_dict(data) - Static method to deserialize from dictionary

parse_m3u(content) function

From m3u_parser.py:35-145, this function parses M3U content line-by-line:Supported directives:
  • #EXTINF - Extracts channel title and attributes (tvg-logo, group-title) from m3u_parser.py:53-66
  • #EXTVLCOPT - Extracts VLC options (http-user-agent, http-referrer) from m3u_parser.py:68-73
  • #EXTHTTP - Parses JSON headers (cookie, user-agent) from m3u_parser.py:75-85
  • #KODIPROP:inputstream.adaptive.license_key - Extracts DRM license strings from m3u_parser.py:87-89
  • URL lines - Stream URLs with optional pipe-separated parameters from m3u_parser.py:121-140
URL parameter parsing:
if "|" in full_url_line:
  url_parts = full_url_line.split("|")
  current_item.url = url_parts[0]
  params = url_parts[1].split("&")
  for p in params:
    if "=" in p:
      k, v = p.split("=", 1)
      if k.lower() == "user-agent":
        current_item.user_agent = v
      elif k.lower() == "referer":
        current_item.referer = v
      # ... more parameter handling

Usage

from lib.m3u_parser import parse_m3u

m3u_content = """#EXTM3U
#EXTINF:-1 tvg-logo="logo.png" group-title="Sports",Channel Name
http://example.com/stream.m3u8
"""

channels = parse_m3u(m3u_content)
for channel in channels:
  print(f"{channel.title}: {channel.url}")
The crypto utilities module provides AES-256-CBC decryption for encrypted provider data and playlists.

Purpose

Decrypts encrypted content from the Cricfy backend using multiple AES keys with automatic fallback.

Key management

From crypto_utils.py:9-39:
SECRET1_FILE_PATH = ADDON_PATH / "resources" / "secret1.txt"
SECRET2_FILE_PATH = ADDON_PATH / "resources" / "secret2.txt"
SECRET1 = SECRET1_FILE_PATH.read_text(encoding="utf-8").strip()
SECRET2 = SECRET2_FILE_PATH.read_text(encoding="utf-8").strip()

def parse_key_info(secret: str) -> KeyInfo:
  key_hex, iv_hex = secret.split(":")
  return KeyInfo(
    key=hex_string_to_bytes(key_hex),
    iv=hex_string_to_bytes(iv_hex),
  )
Keys are stored in hex format as key:iv pairs and parsed into KeyInfo dataclass instances.

Decryption functions

decrypt_data(encrypted_base64) from crypto_utils.py:42-63
def decrypt_data(encrypted_base64: str) -> Optional[str]:
  clean_base64 = encrypted_base64.strip().replace("\n", "").replace("\r", "")
  ciphertext = base64.b64decode(clean_base64)
  
  for key_info in keys().values():
    result = try_decrypt(ciphertext, key_info)
    if result is not None:
      return result
  
  return None
Decrypts Base64-encoded provider data. Tries all available keys until successful.decrypt_content(content) from crypto_utils.py:88-128
def decrypt_content(content: str) -> str:
  # Check if already plain M3U
  if content.startswith("#EXTM3U") or content.startswith("#EXTINF"):
    return content
  
  # Extract encrypted parts
  part1 = trimmed_content[0:10]
  part2 = trimmed_content[34:-54]
  part3 = trimmed_content[-10:]
  encrypted_data_str = part1 + part2 + part3
  
  iv_base64 = trimmed_content[10:34]
  key_base64 = trimmed_content[-54:-10]
  
  # Decrypt using extracted key and IV
  cipher = AES.new(key, AES.MODE_CBC, iv)
  decrypted_padded = cipher.decrypt(encrypted_bytes)
  decrypted_data = unpad(decrypted_padded, AES.block_size)
  
  return decrypted_data.decode('utf-8')
Decrypts M3U playlist content with embedded key/IV. Falls back to original content if decryption fails.try_decrypt(ciphertext, key_info) from crypto_utils.py:66-85Attempts decryption with a specific key and validates the result.

Usage

from lib.crypto_utils import decrypt_data, decrypt_content

# Decrypt provider data
encrypted_providers = fetch_url("https://api.example.com/cats.txt")
providers_json = decrypt_data(encrypted_providers)

# Decrypt M3U content
encrypted_m3u = fetch_url("https://provider.com/playlist.m3u")
plain_m3u = decrypt_content(encrypted_m3u)
The Remote Config module fetches dynamic configuration from Firebase Remote Config.

Purpose

Retrieves API endpoint URLs from Firebase, allowing backend changes without updating the addon.

Credits

Based on the implementation from NivinCNC’s CNCVerse CloudStream Extension.

Configuration loading

From remote_config.py:15-24:
CRICFY_PROPERTIES_FILE_PATH = ADDON_PATH / "resources" / "cricfy_properties.json"
CRICFY_PROPERTIES = json.loads(
  CRICFY_PROPERTIES_FILE_PATH.read_text(encoding="utf-8"))

CRICFY_PACKAGE_NAME = CRICFY_PROPERTIES.get("cricfy_package_name")
CRICFY_FIREBASE_API_KEY = CRICFY_PROPERTIES.get("cricfy_firebase_api_key")
CRICFY_FIREBASE_APP_ID = CRICFY_PROPERTIES.get("cricfy_firebase_app_id")
Loads Firebase credentials from the properties file.

Key functions

fetch_remote_config() from remote_config.py:32-91
def fetch_remote_config():
  url = f"https://firebaseremoteconfig.googleapis.com/v1/projects/{PROJECT_NUMBER}/namespaces/firebase:fetch"
  app_instance_id = _get_random_instance_id()
  
  payload = {
    "appInstanceId": app_instance_id,
    "appId": CRICFY_FIREBASE_APP_ID,
    "packageName": CRICFY_PACKAGE_NAME,
    # ... more metadata
  }
  
  headers = {
    "X-Android-Package": CRICFY_PACKAGE_NAME,
    "X-Goog-Api-Key": CRICFY_FIREBASE_API_KEY,
  }
  
  response = requests.post(url, headers=headers, json=payload, timeout=30)
  return response.json().get("entries")
Fetches Firebase Remote Config entries with Android app spoofing.get_provider_api_url() from remote_config.py:94-111
def get_provider_api_url():
  try_count = 1
  entries = None
  
  while try_count <= 3:
    entries = fetch_remote_config()
    if entries:
      break
    try_count += 1
  
  return entries.get("cric_api2") or entries.get("cric_api1")
Retrieves the provider API URL with retry logic. Prioritizes cric_api2 over cric_api1.

Usage

from lib.remote_config import get_provider_api_url

api_url = get_provider_api_url()
if api_url:
  providers_response = fetch_url(f"{api_url}/cats.txt")
The request utilities module provides HTTP fetching with custom headers.

Purpose

Standardizes HTTP requests across the plugin with appropriate headers for compatibility.

Custom headers

From req.py:4-13:
custom_headers = {
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
  "Accept": "*/*",
  "Cache-Control": "no-cache, no-store",
}

license_headers = {
  "Content-Type": "*/*",
  "User-Agent": "Dalvik/2.1.0 (Linux; U; Android)",
}
custom_headers - Used for M3U playlist fetching license_headers - Used for DRM license server requests (exposed for use in main.py:7)

fetch_url function

From req.py:16-25:
def fetch_url(url: str, timeout: int = 15) -> str:
  response = requests.get(
    url=url,
    headers=custom_headers,
    timeout=timeout,
  )
  response.raise_for_status()
  if response.status_code != 200:
    return ""
  return response.text
Fetches URL content with custom headers and timeout. Raises exception on HTTP errors.

Usage

from lib.req import fetch_url, license_headers

# Fetch playlist
content = fetch_url("https://provider.com/playlist.m3u", timeout=30)

# Use license headers for DRM
li.setProperty('inputstream.adaptive.drm_legacy', 
               f"org.w3.clearkey|{url}|{urlencode(license_headers)}")
The logger module provides standardized logging for the Kodi addon.

Purpose

Simplifies logging with component-based prefixes for easier debugging.

Functions

log_error(component, message) from logger.py:4-5
def log_error(component: str, message: str) -> None:
  xbmc.log(f"Cricfy Plugin [{component}]: {message}", xbmc.LOGERROR)
Logs error messages to Kodi’s log with ERROR level.log_info(component, message) from logger.py:8-9
def log_info(component: str, message: str) -> None:
  xbmc.log(f"Cricfy Plugin [{component}]: {message}", xbmc.LOGINFO)
Logs informational messages to Kodi’s log with INFO level.

Usage

from lib.logger import log_error, log_info

log_info("providers", "Fetching providers from remote URL")
log_error("crypto_utils", f"Decryption failed: {e}")

Log output format

Cricfy Plugin [providers]: Fetching providers from remote URL
Cricfy Plugin [crypto_utils]: Decryption failed: invalid padding
Component names help identify which module produced each log entry.

Module dependencies

The modules have the following dependency structure:
main.py
├── lib.providers (get_providers, get_channels)
├── lib.req (license_headers)
└── lib.logger (log_error)

service.py
├── lib.config (cache)
├── lib.logger (log_info)
└── lib.providers (get_providers)

lib/providers.py
├── lib.config (cache)
├── lib.logger (log_error, log_info)
├── lib.crypto_utils (decrypt_content, decrypt_data)
├── lib.req (fetch_url)
├── lib.m3u_parser (PlaylistItem, parse_m3u)
└── lib.remote_config (get_provider_api_url)

lib/crypto_utils.py
├── lib.config (ADDON_PATH)
└── lib.logger (log_error)

lib/remote_config.py
├── lib.config (ADDON_PATH)
└── lib.logger (log_error)

lib/m3u_parser.py
└── (no dependencies)

lib/req.py
└── (no dependencies)

lib/logger.py
└── (no dependencies)

lib/config.py
└── (no dependencies)
When modifying modules, be aware of these dependencies to avoid circular imports and breaking changes.

Build docs developers (and LLMs) love