HLSUtils.ts

import { URL } from 'node:url';
import UnsupportedProtocol from './exceptions/UnsupportedProtocol.js';

/**
 * @category Utils
 * @author Nur Rony<pro.nmrony@gmail.com>
 * @description Static utility helper for HLS operations and URL validation.
 */
export class Utils {
  /**
   * Validates if the provided string is a properly formatted URL with supported protocols.
   * @param url - The URL string to validate.
   * @param protocols - An array of allowed protocols.
   * @throws {UnsupportedProtocol} If the protocol is not in the allowed list.
   * @returns `true` if valid.
   */
  static isValidUrl(
    url: string,
    protocols: string[] = ['http:', 'https:', 'ftp:', 'sftp:']
  ): boolean | UnsupportedProtocol {
    const { protocol } = new URL(url);
    if (protocol && !protocols.includes(protocol)) {
      throw new UnsupportedProtocol(`${protocol} is not supported. Supported protocols are ${protocols.join(', ')}`);
    }
    return true;
  }

  /**
   * Removes the leading slash from a pathname for safe path joining.
   * @param url - The string to strip.
   * @returns The string without a leading slash.
   */
  static stripFirstSlash(url: string): string {
    return url.startsWith('/') ? url.substring(1) : url;
  }

  /**
   * Checks if the content starts with the mandatory HLS #EXTM3U tag.
   * @param content - The raw manifest string.
   * @returns `true` if it contains the HLS header.
   */
  static isValidPlaylist(content: string): boolean {
    return /^#EXTM3U/im.test(content);
  }

  /**
   * Utility to create a URL object from a string.
   * @param url - The URL string to parse.
   * @returns A native Node/Web URL object.
   */
  static parseUrl(url: string): URL {
    return new URL(url);
  }

  /**
   * Filters out specific keys from an object.
   * @param subject - The source object.
   * @param keys - The keys to remove.
   * @returns A new object excluding the specified keys.
   */
  static omit<T extends Record<string, any>, K extends keyof T>(subject: T, ...keys: (K | K[])[]): Omit<T, K> {
    const keysToRemove = new Set(keys.flat());
    return Object.fromEntries(Object.entries(subject).filter(([key]) => !keysToRemove.has(key as K))) as Omit<T, K>;
  }

  /**
   * Validation check to ensure provided hooks are executable functions.
   * @param fn - The value to check.
   * @returns `true` if the value is NOT a function.
   */
  static isNotFunction(fn: unknown): boolean {
    return typeof fn !== 'function';
  }
}