     1→import Foundation
     2→
     3→/// Service for downloading audio from YouTube videos
     4→actor AudioDownloadService {
     5→    static let shared = AudioDownloadService()
     6→
     7→    private init() {}
     8→
     9→    /// Downloads audio from YouTube video as MP3
    10→    /// - Parameters:
    11→    ///   - url: YouTube video URL
    12→    ///   - progress: Progress callback with status messages
    13→    /// - Returns: URL to the downloaded MP3 file
    14→    func downloadAudio(
    15→        url: String,
    16→        progress: @escaping @Sendable (String) -> Void
    17→    ) async throws -> URL {
    18→        guard let videoID = MCPService.extractVideoID(from: url) else {
    19→            throw AudioDownloadError.invalidURL
    20→        }
    21→
    22→        await MainActor.run { progress("🎵 Baixando áudio do vídeo...") }
    23→        await AppLogger.shared.info("Starting audio download for video: \(videoID)", category: .transcript)
    24→
    25→        // Create temp directory
    26→        let tempDir = FileManager.default.temporaryDirectory
    27→            .appendingPathComponent("youtube-audio", isDirectory: true)
    28→        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
    29→
    30→        let outputPath = tempDir.appendingPathComponent("\(videoID).mp3")
    31→
    32→        // Remove existing file
    33→        try? FileManager.default.removeItem(at: outputPath)
    34→
    35→        // Download using pytubefix (primary) with yt-dlp fallback
    36→        let ffmpegPath = AppConfig.Executables.ffmpegPath
    37→        let escapedURL = url.replacingOccurrences(of: "'", with: "\\'")
    38→        let escapedOutputPath = outputPath.path.replacingOccurrences(of: "'", with: "\\'")
    39→
    40→        let script = """
    41→        import sys
    42→        sys.stdout.reconfigure(line_buffering=True)
    43→        import os
    44→        import subprocess
    45→
    46→        url = '\(escapedURL)'
    47→        output_path = '\(escapedOutputPath)'
    48→        output_template = output_path.replace('.mp3', '')
    49→        ffmpeg_cmd = '\(ffmpegPath)'
    50→
    51→        def progress_hook(d):
    52→            if d['status'] == 'downloading':
    53→                percent = d.get('_percent_str', 'N/A')
    54→                print(f"PROGRESS: Baixando... {percent}", flush=True)
    55→            elif d['status'] == 'finished':
    56→                print("PROGRESS: Download concluído, convertendo para MP3...", flush=True)
    57→
    58→        # Strategy 0: Try pytubefix (most reliable, bypasses YouTube restrictions)
    59→        def try_pytubefix():
    60→            try:
    61→                from pytubefix import YouTube
    62→                print("PROGRESS: Baixando via pytubefix...", flush=True)
    63→
    64→                yt = YouTube(url)
    65→                print(f"PROGRESS: Vídeo: {yt.title[:50]}...", flush=True)
    66→
    67→                # Get best audio stream
    68→                audio_stream = yt.streams.filter(only_audio=True).order_by('abr').desc().first()
    69→                if not audio_stream:
    70→                    print("PROGRESS: Nenhum stream de áudio encontrado", flush=True)
    71→                    return False
    72→
    73→                print(f"PROGRESS: Stream: {audio_stream.abr}", flush=True)
    74→
    75→                # Download to temp file
    76→                temp_file = audio_stream.download(
    77→                    output_path=os.path.dirname(output_path),
    78→                    filename=os.path.basename(output_template)
    79→                )
    80→
    81→                if not temp_file or not os.path.exists(temp_file):
    82→                    return False
    83→
    84→                # Convert to MP3 using ffmpeg
    85→                print("PROGRESS: Convertendo para MP3...", flush=True)
    86→
    87→                result = subprocess.run([
    88→                    ffmpeg_cmd, '-y', '-i', temp_file,
    89→                    '-vn', '-acodec', 'libmp3lame', '-ab', '192k',
    90→                    output_path
    91→                ], capture_output=True, text=True)
    92→
    93→                # Remove temp file
    94→                if temp_file != output_path:
    95→                    try:
    96→                        os.remove(temp_file)
    97→                    except:
    98→                        pass
    99→
   100→                return os.path.exists(output_path)
   101→            except ImportError:
   102→                print("PROGRESS: pytubefix não instalado, tentando alternativas...", flush=True)
   103→                return False
   104→            except Exception as e:
   105→                print(f"PROGRESS: pytubefix falhou: {str(e)[:50]}...", flush=True)
   106→                return False
   107→
   108→        # Strategy 1: Try with browser cookies (Chrome/Firefox/Safari)
   109→        def try_with_cookies():
   110→            browsers = ['chrome', 'firefox', 'safari']
   111→            for browser in browsers:
   112→                try:
   113→                    print(f"PROGRESS: Tentando com cookies do {browser.title()}...", flush=True)
   114→                    ydl_opts = {
   115→                        'format': 'bestaudio/best',
   116→                        'postprocessors': [{
   117→                            'key': 'FFmpegExtractAudio',
   118→                            'preferredcodec': 'mp3',
   119→                            'preferredquality': '192',
   120→                        }],
   121→                        'outtmpl': output_template,
   122→                        'quiet': True,
   123→                        'no_warnings': True,
   124→                        'cookiesfrombrowser': (browser,),
   125→                        'progress_hooks': [progress_hook],
   126→                    }
   127→                    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
   128→                        ydl.download([url])
   129→                    return True
   130→                except Exception as e:
   131→                    error_msg = str(e).lower()
   132→                    if 'permission' in error_msg or 'cookies' in error_msg:
   133→                        continue  # Try next browser
   134→                    if '403' not in error_msg and 'forbidden' not in error_msg:
   135→                        raise  # Re-raise non-403 errors
   136→            return False
   137→
   138→        # Strategy 2: Try with different player clients
   139→        def try_with_clients():
   140→            clients = ['web_creator', 'tv', 'mweb']
   141→            for client in clients:
   142→                try:
   143→                    print(f"PROGRESS: Tentando cliente {client}...", flush=True)
   144→                    ydl_opts = {
   145→                        'format': 'bestaudio/best',
   146→                        'postprocessors': [{
   147→                            'key': 'FFmpegExtractAudio',
   148→                            'preferredcodec': 'mp3',
   149→                            'preferredquality': '192',
   150→                        }],
   151→                        'outtmpl': output_template,
   152→                        'quiet': True,
   153→                        'no_warnings': True,
   154→                        'extractor_args': {'youtube': {'player_client': [client]}},
   155→                        'progress_hooks': [progress_hook],
   156→                    }
   157→                    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
   158→                        ydl.download([url])
   159→                    return True
   160→                except Exception as e:
   161→                    error_msg = str(e).lower()
   162→                    if '403' not in error_msg and 'forbidden' not in error_msg:
   163→                        raise
   164→            return False
   165→
   166→        # Strategy 3: Try format 18 (360p with embedded audio) as last resort
   167→        def try_format_18():
   168→            try:
   169→                print("PROGRESS: Tentando formato alternativo (360p)...", flush=True)
   170→                ydl_opts = {
   171→                    'format': '18/worst[ext=mp4]',  # 360p mp4 with audio
   172→                    'postprocessors': [{
   173→                        'key': 'FFmpegExtractAudio',
   174→                        'preferredcodec': 'mp3',
   175→                        'preferredquality': '192',
   176→                    }],
   177→                    'outtmpl': output_template,
   178→                    'quiet': True,
   179→                    'no_warnings': True,
   180→                    'progress_hooks': [progress_hook],
   181→                }
   182→                with yt_dlp.YoutubeDL(ydl_opts) as ydl:
   183→                    ydl.download([url])
   184→                return True
   185→            except:
   186→                return False
   187→
   188→        # Execute strategies in order of reliability
   189→        try:
   190→            print("PROGRESS: Iniciando download do áudio...", flush=True)
   191→
   192→            success = False
   193→
   194→            # Strategy 0: pytubefix (most reliable, bypasses most restrictions)
   195→            if not success:
   196→                try:
   197→                    success = try_pytubefix()
   198→                except Exception as e:
   199→                    print(f"PROGRESS: pytubefix falhou: {str(e)[:50]}...", flush=True)
   200→
   201→            # Strategy 1: Try cookies (if pytubefix fails)
   202→            if not success:
   203→                try:
   204→                    import yt_dlp  # Only import if needed
   205→                    success = try_with_cookies()
   206→                except ImportError:
   207→                    print("PROGRESS: yt-dlp não instalado", flush=True)
   208→                except Exception as e:
   209→                    print(f"PROGRESS: Cookies falharam: {str(e)[:50]}...", flush=True)
   210→
   211→            # Strategy 2: Try different clients
   212→            if not success:
   213→                try:
   214→                    import yt_dlp
   215→                    success = try_with_clients()
   216→                except ImportError:
   217→                    pass
   218→                except Exception as e:
   219→                    print(f"PROGRESS: Clientes falharam: {str(e)[:50]}...", flush=True)
   220→
   221→            # Strategy 3: Try format 18 as last resort
   222→            if not success:
   223→                try:
   224→                    import yt_dlp
   225→                    success = try_format_18()
   226→                except ImportError:
   227→                    pass
   228→
   229→            if success and os.path.exists(output_path):
   230→                print("PROGRESS: Download concluído!", flush=True)
   231→                print(f"OUTPUT_PATH: {output_path}")
   232→            else:
   233→                print("ERROR: Todas as estratégias de download falharam. Verifique se pytubefix está instalado: pip3 install pytubefix")
   234→
   235→        except Exception as e:
   236→            print(f"ERROR: {str(e)}")
   237→        """
   238→
   239→        let output = try await runPythonWithProgress(
   240→            script: script,
   241→            timeout: 900, // 15 minutes (more time for retry strategies)
   242→            progress: progress
   243→        )
   244→
   245→        if output.contains("ERROR:") {
   246→            let errorMsg = output.replacingOccurrences(of: "ERROR: ", with: "")
   247→            await AppLogger.shared.error("Audio download failed: \(errorMsg)", category: .transcript)
   248→            throw AudioDownloadError.downloadFailed(errorMsg)
   249→        }
   250→
   251→        // Verify file exists
   252→        guard FileManager.default.fileExists(atPath: outputPath.path) else {
   253→            await AppLogger.shared.error("Audio file not found after download", category: .transcript)
   254→            throw AudioDownloadError.fileNotFound
   255→        }
   256→
   257→        let fileSize = try? FileManager.default.attributesOfItem(atPath: outputPath.path)[.size] as? Int64
   258→        await AppLogger.shared.info("Audio downloaded successfully", category: .transcript, metadata: [
   259→            "path": outputPath.path,
   260→            "size_bytes": String(fileSize ?? 0)
   261→        ])
   262→
   263→        await MainActor.run { progress("✅ Áudio baixado com sucesso!") }
   264→
   265→        return outputPath
   266→    }
   267→
   268→    // MARK: - Process Data Buffer
   269→
   270→    /// Actor-based buffer for thread-safe process output collection
   271→    private actor ProcessDataBuffer {
   272→        private var data = Data()
   273→        private var isCompleted = false
   274→
   275→        func append(_ newData: Data) {
   276→            data.append(newData)
   277→        }
   278→
   279→        func getData() -> Data {
   280→            return data
   281→        }
   282→
   283→        func markCompleted() -> Bool {
   284→            if isCompleted {
   285→                return true  // Already completed
   286→            }
   287→            isCompleted = true
   288→            return false  // First to complete
   289→        }
   290→
   291→        func checkCompleted() -> Bool {
   292→            return isCompleted
   293→        }
   294→    }
   295→
   296→    /// Helper method to run Python script with progress feedback
   297→    private func runPythonWithProgress(
   298→        script: String,
   299→        timeout: TimeInterval,
   300→        progress: @escaping @Sendable (String) -> Void
   301→    ) async throws -> String {
   302→        return try await withCheckedThrowingContinuation { continuation in
   303→            let process = Process()
   304→            // Use arch -arm64 to ensure native ARM execution
   305→            process.executableURL = URL(fileURLWithPath: "/usr/bin/arch")
   306→            process.arguments = ["-arm64", AppConfig.Executables.pythonPath, "-c", script]
   307→
   308→            let outputPipe = Pipe()
   309→            let errorPipe = Pipe()
   310→            process.standardOutput = outputPipe
   311→            process.standardError = errorPipe
   312→
   313→            // ✅ Actor-isolated buffer (thread-safe)
   314→            let buffer = ProcessDataBuffer()
   315→
   316→            // Read output in real-time for progress updates
   317→            outputPipe.fileHandleForReading.readabilityHandler = { handle in
   318→                let data = handle.availableData
   319→                if !data.isEmpty {
   320→                    Task {
   321→                        await buffer.append(data)
   322→                    }
   323→
   324→                    if let text = String(data: data, encoding: .utf8) {
   325→                        let lines = text.components(separatedBy: "\n")
   326→                        for line in lines {
   327→                            if line.hasPrefix("PROGRESS: ") {
   328→                                let message = String(line.dropFirst(10))
   329→                                Task { @MainActor in
   330→                                    progress("📡 \(message)")
   331→                                }
   332→                            }
   333→                        }
   334→                    }
   335→                }
   336→            }
   337→
   338→            // Timeout timer
   339→            let timeoutTimer = DispatchSource.makeTimerSource(queue: .global())
   340→            timeoutTimer.schedule(deadline: .now() + timeout)
   341→            timeoutTimer.setEventHandler {
   342→                Task {
   343→                    let alreadyCompleted = await buffer.checkCompleted()
   344→                    if !alreadyCompleted {
   345→                        _ = await buffer.markCompleted()
   346→                        process.terminate()
   347→                        Task { @MainActor in
   348→                            progress("⚠ Timeout - operação demorou mais de \(Int(timeout/60)) minutos")
   349→                        }
   350→                        continuation.resume(throwing: AudioDownloadError.timeout)
   351→                    }
   352→                }
   353→            }
   354→            timeoutTimer.resume()
   355→
   356→            process.terminationHandler = { proc in
   357→                timeoutTimer.cancel()
   358→                outputPipe.fileHandleForReading.readabilityHandler = nil
   359→
   360→                Task {
   361→                    let alreadyCompleted = await buffer.markCompleted()
   362→                    if alreadyCompleted {
   363→                        return  // Timeout already handled
   364→                    }
   365→
   366→                    // Read any remaining data
   367→                    let remainingData = outputPipe.fileHandleForReading.readDataToEndOfFile()
   368→                    await buffer.append(remainingData)
   369→
   370→                    let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
   371→
   372→                    if proc.terminationStatus != 0 {
   373→                        let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error"
   374→                        continuation.resume(throwing: AudioDownloadError.processError(errorString))
   375→                    } else {
   376→                        let outputData = await buffer.getData()
   377→                        guard let output = String(data: outputData, encoding: .utf8) else {
   378→                            continuation.resume(throwing: AudioDownloadError.invalidResponse)
   379→                            return
   380→                        }
   381→                        continuation.resume(returning: output)
   382→                    }
   383→                }
   384→            }
   385→
   386→            do {
   387→                try process.run()
   388→            } catch {
   389→                timeoutTimer.cancel()
   390→                continuation.resume(throwing: AudioDownloadError.processError(error.localizedDescription))
   391→            }
   392→        }
   393→    }
   394→}
   395→
   396→// MARK: - Errors
   397→
   398→enum AudioDownloadError: LocalizedError {
   399→    case invalidURL
   400→    case downloadFailed(String)
   401→    case fileNotFound
   402→    case timeout
   403→    case processError(String)
   404→    case invalidResponse
   405→    case youtubeBlocked
   406→
   407→    var errorDescription: String? {
   408→        switch self {
   409→        case .invalidURL:
   410→            return "URL do YouTube inválida"
   411→        case .downloadFailed(let message):
   412→            if message.contains("403") || message.contains("Forbidden") || message.contains("bloqueando") {
   413→                return """
   414→                ⚠ YouTube está bloqueando o download.
   415→
   416→                Possíveis soluções:
   417→                • Faça login no YouTube no Chrome/Firefox
   418→                • Aguarde alguns minutos e tente novamente
   419→                • O vídeo pode ter restrições de download
   420→                """
   421→            }
   422→            return "Falha ao baixar áudio: \(message)"
   423→        case .fileNotFound:
   424→            return "Arquivo de áudio não encontrado após download"
   425→        case .timeout:
   426→            return "Timeout ao baixar áudio. Vídeo pode ser muito grande."
   427→        case .processError(let message):
   428→            return "Erro no processo de download: \(message)"
   429→        case .invalidResponse:
   430→            return "Resposta inválida do script de download"
   431→        case .youtubeBlocked:
   432→            return """
   433→            ⚠ YouTube está bloqueando downloads temporariamente.
   434→
   435→            Tente:
   436→            1. Fazer login no YouTube no seu navegador
   437→            2. Aguardar alguns minutos
   438→            3. Tentar outro vídeo
   439→            """
   440→        }
   441→    }
   442→}
   443→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

/// Service for downloading audio from YouTube videos
actor AudioDownloadService {
    static let shared = AudioDownloadService()

    private init() {}

    /// Downloads audio from YouTube video as MP3
    /// - Parameters:
    ///   - url: YouTube video URL
    ///   - progress: Progress callback with status messages
    /// - Returns: URL to the downloaded MP3 file
    func downloadAudio(
        url: String,
        progress: @escaping @Sendable (String) -> Void
    ) async throws -> URL {
        guard let videoID = MCPService.extractVideoID(from: url) else {
            throw AudioDownloadError.invalidURL
        }

        await MainActor.run { progress("🎵 Baixando áudio do vídeo...") }
        await AppLogger.shared.info("Starting audio download for video: \(videoID)", category: .transcript)

        // Create temp directory
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("youtube-audio", isDirectory: true)
        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)

        let outputPath = tempDir.appendingPathComponent("\(videoID).mp3")

        // Remove existing file
        try? FileManager.default.removeItem(at: outputPath)

        // Download using pytubefix (primary) with yt-dlp fallback
        let ffmpegPath = AppConfig.Executables.ffmpegPath
        let escapedURL = url.replacingOccurrences(of: "'", with: "\\'")
        let escapedOutputPath = outputPath.path.replacingOccurrences(of: "'", with: "\\'")

        let script = """
        import sys
        sys.stdout.reconfigure(line_buffering=True)
        import os
        import subprocess

        url = '\(escapedURL)'
        output_path = '\(escapedOutputPath)'
        output_template = output_path.replace('.mp3', '')
        ffmpeg_cmd = '\(ffmpegPath)'

        def progress_hook(d):
            if d['status'] == 'downloading':
                percent = d.get('_percent_str', 'N/A')
                print(f"PROGRESS: Baixando... {percent}", flush=True)
            elif d['status'] == 'finished':
                print("PROGRESS: Download concluído, convertendo para MP3...", flush=True)

        # Strategy 0: Try pytubefix (most reliable, bypasses YouTube restrictions)
        def try_pytubefix():
            try:
                from pytubefix import YouTube
                print("PROGRESS: Baixando via pytubefix...", flush=True)

                yt = YouTube(url)
                print(f"PROGRESS: Vídeo: {yt.title[:50]}...", flush=True)

                # Get best audio stream
                audio_stream = yt.streams.filter(only_audio=True).order_by('abr').desc().first()
                if not audio_stream:
                    print("PROGRESS: Nenhum stream de áudio encontrado", flush=True)
                    return False

                print(f"PROGRESS: Stream: {audio_stream.abr}", flush=True)

                # Download to temp file
                temp_file = audio_stream.download(
                    output_path=os.path.dirname(output_path),
                    filename=os.path.basename(output_template)
                )

                if not temp_file or not os.path.exists(temp_file):
                    return False

                # Convert to MP3 using ffmpeg
                print("PROGRESS: Convertendo para MP3...", flush=True)

                result = subprocess.run([
                    ffmpeg_cmd, '-y', '-i', temp_file,
                    '-vn', '-acodec', 'libmp3lame', '-ab', '192k',
                    output_path
                ], capture_output=True, text=True)

                # Remove temp file
                if temp_file != output_path:
                    try:
                        os.remove(temp_file)
                    except:
                        pass

                return os.path.exists(output_path)
            except ImportError:
                print("PROGRESS: pytubefix não instalado, tentando alternativas...", flush=True)
                return False
            except Exception as e:
                print(f"PROGRESS: pytubefix falhou: {str(e)[:50]}...", flush=True)
                return False

        # Strategy 1: Try with browser cookies (Chrome/Firefox/Safari)
        def try_with_cookies():
            browsers = ['chrome', 'firefox', 'safari']
            for browser in browsers:
                try:
                    print(f"PROGRESS: Tentando com cookies do {browser.title()}...", flush=True)
                    ydl_opts = {
                        'format': 'bestaudio/best',
                        'postprocessors': [{
                            'key': 'FFmpegExtractAudio',
                            'preferredcodec': 'mp3',
                            'preferredquality': '192',
                        }],
                        'outtmpl': output_template,
                        'quiet': True,
                        'no_warnings': True,
                        'cookiesfrombrowser': (browser,),
                        'progress_hooks': [progress_hook],
                    }
                    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                        ydl.download([url])
                    return True
                except Exception as e:
                    error_msg = str(e).lower()
                    if 'permission' in error_msg or 'cookies' in error_msg:
                        continue  # Try next browser
                    if '403' not in error_msg and 'forbidden' not in error_msg:
                        raise  # Re-raise non-403 errors
            return False

        # Strategy 2: Try with different player clients
        def try_with_clients():
            clients = ['web_creator', 'tv', 'mweb']
            for client in clients:
                try:
                    print(f"PROGRESS: Tentando cliente {client}...", flush=True)
                    ydl_opts = {
                        'format': 'bestaudio/best',
                        'postprocessors': [{
                            'key': 'FFmpegExtractAudio',
                            'preferredcodec': 'mp3',
                            'preferredquality': '192',
                        }],
                        'outtmpl': output_template,
                        'quiet': True,
                        'no_warnings': True,
                        'extractor_args': {'youtube': {'player_client': [client]}},
                        'progress_hooks': [progress_hook],
                    }
                    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                        ydl.download([url])
                    return True
                except Exception as e:
                    error_msg = str(e).lower()
                    if '403' not in error_msg and 'forbidden' not in error_msg:
                        raise
            return False

        # Strategy 3: Try format 18 (360p with embedded audio) as last resort
        def try_format_18():
            try:
                print("PROGRESS: Tentando formato alternativo (360p)...", flush=True)
                ydl_opts = {
                    'format': '18/worst[ext=mp4]',  # 360p mp4 with audio
                    'postprocessors': [{
                        'key': 'FFmpegExtractAudio',
                        'preferredcodec': 'mp3',
                        'preferredquality': '192',
                    }],
                    'outtmpl': output_template,
                    'quiet': True,
                    'no_warnings': True,
                    'progress_hooks': [progress_hook],
                }
                with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                    ydl.download([url])
                return True
            except:
                return False

        # Execute strategies in order of reliability
        try:
            print("PROGRESS: Iniciando download do áudio...", flush=True)

            success = False

            # Strategy 0: pytubefix (most reliable, bypasses most restrictions)
            if not success:
                try:
                    success = try_pytubefix()
                except Exception as e:
                    print(f"PROGRESS: pytubefix falhou: {str(e)[:50]}...", flush=True)

            # Strategy 1: Try cookies (if pytubefix fails)
            if not success:
                try:
                    import yt_dlp  # Only import if needed
                    success = try_with_cookies()
                except ImportError:
                    print("PROGRESS: yt-dlp não instalado", flush=True)
                except Exception as e:
                    print(f"PROGRESS: Cookies falharam: {str(e)[:50]}...", flush=True)

            # Strategy 2: Try different clients
            if not success:
                try:
                    import yt_dlp
                    success = try_with_clients()
                except ImportError:
                    pass
                except Exception as e:
                    print(f"PROGRESS: Clientes falharam: {str(e)[:50]}...", flush=True)

            # Strategy 3: Try format 18 as last resort
            if not success:
                try:
                    import yt_dlp
                    success = try_format_18()
                except ImportError:
                    pass

            if success and os.path.exists(output_path):
                print("PROGRESS: Download concluído!", flush=True)
                print(f"OUTPUT_PATH: {output_path}")
            else:
                print("ERROR: Todas as estratégias de download falharam. Verifique se pytubefix está instalado: pip3 install pytubefix")

        except Exception as e:
            print(f"ERROR: {str(e)}")
        """

        let output = try await runPythonWithProgress(
            script: script,
            timeout: 900, // 15 minutes (more time for retry strategies)
            progress: progress
        )

        if output.contains("ERROR:") {
            let errorMsg = output.replacingOccurrences(of: "ERROR: ", with: "")
            await AppLogger.shared.error("Audio download failed: \(errorMsg)", category: .transcript)
            throw AudioDownloadError.downloadFailed(errorMsg)
        }

        // Verify file exists
        guard FileManager.default.fileExists(atPath: outputPath.path) else {
            await AppLogger.shared.error("Audio file not found after download", category: .transcript)
            throw AudioDownloadError.fileNotFound
        }

        let fileSize = try? FileManager.default.attributesOfItem(atPath: outputPath.path)[.size] as? Int64
        await AppLogger.shared.info("Audio downloaded successfully", category: .transcript, metadata: [
            "path": outputPath.path,
            "size_bytes": String(fileSize ?? 0)
        ])

        await MainActor.run { progress("✅ Áudio baixado com sucesso!") }

        return outputPath
    }

    // MARK: - Process Data Buffer

    /// Actor-based buffer for thread-safe process output collection
    private actor ProcessDataBuffer {
        private var data = Data()
        private var isCompleted = false

        func append(_ newData: Data) {
            data.append(newData)
        }

        func getData() -> Data {
            return data
        }

        func markCompleted() -> Bool {
            if isCompleted {
                return true  // Already completed
            }
            isCompleted = true
            return false  // First to complete
        }

        func checkCompleted() -> Bool {
            return isCompleted
        }
    }

    /// Helper method to run Python script with progress feedback
    private func runPythonWithProgress(
        script: String,
        timeout: TimeInterval,
        progress: @escaping @Sendable (String) -> Void
    ) async throws -> String {
        return try await withCheckedThrowingContinuation { continuation in
            let process = Process()
            // Use arch -arm64 to ensure native ARM execution
            process.executableURL = URL(fileURLWithPath: "/usr/bin/arch")
            process.arguments = ["-arm64", AppConfig.Executables.pythonPath, "-c", script]

            let outputPipe = Pipe()
            let errorPipe = Pipe()
            process.standardOutput = outputPipe
            process.standardError = errorPipe

            // ✅ Actor-isolated buffer (thread-safe)
            let buffer = ProcessDataBuffer()

            // Read output in real-time for progress updates
            outputPipe.fileHandleForReading.readabilityHandler = { handle in
                let data = handle.availableData
                if !data.isEmpty {
                    Task {
                        await buffer.append(data)
                    }

                    if let text = String(data: data, encoding: .utf8) {
                        let lines = text.components(separatedBy: "\n")
                        for line in lines {
                            if line.hasPrefix("PROGRESS: ") {
                                let message = String(line.dropFirst(10))
                                Task { @MainActor in
                                    progress("📡 \(message)")
                                }
                            }
                        }
                    }
                }
            }

            // Timeout timer
            let timeoutTimer = DispatchSource.makeTimerSource(queue: .global())
            timeoutTimer.schedule(deadline: .now() + timeout)
            timeoutTimer.setEventHandler {
                Task {
                    let alreadyCompleted = await buffer.checkCompleted()
                    if !alreadyCompleted {
                        _ = await buffer.markCompleted()
                        process.terminate()
                        Task { @MainActor in
                            progress("⚠ Timeout - operação demorou mais de \(Int(timeout/60)) minutos")
                        }
                        continuation.resume(throwing: AudioDownloadError.timeout)
                    }
                }
            }
            timeoutTimer.resume()

            process.terminationHandler = { proc in
                timeoutTimer.cancel()
                outputPipe.fileHandleForReading.readabilityHandler = nil

                Task {
                    let alreadyCompleted = await buffer.markCompleted()
                    if alreadyCompleted {
                        return  // Timeout already handled
                    }

                    // Read any remaining data
                    let remainingData = outputPipe.fileHandleForReading.readDataToEndOfFile()
                    await buffer.append(remainingData)

                    let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()

                    if proc.terminationStatus != 0 {
                        let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error"
                        continuation.resume(throwing: AudioDownloadError.processError(errorString))
                    } else {
                        let outputData = await buffer.getData()
                        guard let output = String(data: outputData, encoding: .utf8) else {
                            continuation.resume(throwing: AudioDownloadError.invalidResponse)
                            return
                        }
                        continuation.resume(returning: output)
                    }
                }
            }

            do {
                try process.run()
            } catch {
                timeoutTimer.cancel()
                continuation.resume(throwing: AudioDownloadError.processError(error.localizedDescription))
            }
        }
    }
}

// MARK: - Errors

enum AudioDownloadError: LocalizedError {
    case invalidURL
    case downloadFailed(String)
    case fileNotFound
    case timeout
    case processError(String)
    case invalidResponse
    case youtubeBlocked

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "URL do YouTube inválida"
        case .downloadFailed(let message):
            if message.contains("403") || message.contains("Forbidden") || message.contains("bloqueando") {
                return """
                ⚠ YouTube está bloqueando o download.

                Possíveis soluções:
                • Faça login no YouTube no Chrome/Firefox
                • Aguarde alguns minutos e tente novamente
                • O vídeo pode ter restrições de download
                """
            }
            return "Falha ao baixar áudio: \(message)"
        case .fileNotFound:
            return "Arquivo de áudio não encontrado após download"
        case .timeout:
            return "Timeout ao baixar áudio. Vídeo pode ser muito grande."
        case .processError(let message):
            return "Erro no processo de download: \(message)"
        case .invalidResponse:
            return "Resposta inválida do script de download"
        case .youtubeBlocked:
            return """
            ⚠ YouTube está bloqueando downloads temporariamente.

            Tente:
            1. Fazer login no YouTube no seu navegador
            2. Aguardar alguns minutos
            3. Tentar outro vídeo
            """
        }
    }
}


     1→import Foundation
     2→
     3→// MARK: - Process Utilities
     4→
     5→/// Shared utilities for running Python scripts with progress tracking
     6→enum ProcessUtils {
     7→
     8→    // MARK: - Errors
     9→
    10→    enum ProcessError: Error, LocalizedError {
    11→        case executionFailed(String)
    12→        case timeout
    13→        case invalidOutput
    14→
    15→        var errorDescription: String? {
    16→            switch self {
    17→            case .executionFailed(let message):
    18→                return "Process execution failed: \(message)"
    19→            case .timeout:
    20→                return "Process timed out"
    21→            case .invalidOutput:
    22→                return "Invalid process output"
    23→            }
    24→        }
    25→    }
    26→
    27→    // MARK: - Thread-Safe Buffer
    28→
    29→    /// Thread-safe actor for collecting process output data
    30→    actor ProcessDataBuffer {
    31→        private var data = Data()
    32→
    33→        func append(_ newData: Data) {
    34→            data.append(newData)
    35→        }
    36→
    37→        func getData() -> Data {
    38→            return data
    39→        }
    40→
    41→        func getString() -> String {
    42→            return String(data: data, encoding: .utf8) ?? ""
    43→        }
    44→
    45→        func clear() {
    46→            data = Data()
    47→        }
    48→    }
    49→
    50→    // MARK: - Python Execution
    51→
    52→    /// Runs a Python script with real-time progress updates
    53→    /// - Parameters:
    54→    ///   - script: The Python script to execute
    55→    ///   - timeout: Maximum execution time in seconds (default: 1 hour)
    56→    ///   - progress: Callback for progress updates (lines starting with "PROGRESS: ")
    57→    /// - Returns: The complete output from the script
    58→    static func runPythonWithProgress(
    59→        script: String,
    60→        timeout: TimeInterval = 3600,
    61→        progress: @escaping @Sendable (String) -> Void
    62→    ) async throws -> String {
    63→        return try await withCheckedThrowingContinuation { continuation in
    64→            let process = Process()
    65→            // Use arch -arm64 to ensure native ARM execution
    66→            process.executableURL = URL(fileURLWithPath: "/usr/bin/arch")
    67→            process.arguments = ["-arm64", AppConfig.Executables.pythonPath, "-c", script]
    68→
    69→            let outputPipe = Pipe()
    70→            let errorPipe = Pipe()
    71→            process.standardOutput = outputPipe
    72→            process.standardError = errorPipe
    73→
    74→            let buffer = ProcessDataBuffer()
    75→            var hasResumed = false
    76→            let resumeLock = NSLock()
    77→
    78→            // Safe resume helper to prevent double-resume
    79→            @Sendable func safeResume(with result: Result<String, Error>) {
    80→                resumeLock.lock()
    81→                defer { resumeLock.unlock() }
    82→                guard !hasResumed else { return }
    83→                hasResumed = true
    84→                continuation.resume(with: result)
    85→            }
    86→
    87→            // Read output in real-time
    88→            outputPipe.fileHandleForReading.readabilityHandler = { handle in
    89→                let data = handle.availableData
    90→                if !data.isEmpty {
    91→                    Task {
    92→                        await buffer.append(data)
    93→                    }
    94→
    95→                    if let text = String(data: data, encoding: .utf8) {
    96→                        let lines = text.components(separatedBy: "\n")
    97→                        for line in lines {
    98→                            if line.hasPrefix("PROGRESS: ") {
    99→                                let progressMsg = String(line.dropFirst(10))
   100→                                Task { @MainActor in
   101→                                    progress(progressMsg)
   102→                                }
   103→                            }
   104→                        }
   105→                    }
   106→                }
   107→            }
   108→
   109→            // Timeout handling
   110→            let timeoutTimer = DispatchSource.makeTimerSource(queue: .global())
   111→            timeoutTimer.schedule(deadline: .now() + timeout)
   112→            timeoutTimer.setEventHandler {
   113→                process.terminate()
   114→                safeResume(with: .failure(ProcessError.timeout))
   115→            }
   116→            timeoutTimer.resume()
   117→
   118→            // Process termination handler
   119→            process.terminationHandler = { _ in
   120→                timeoutTimer.cancel()
   121→                outputPipe.fileHandleForReading.readabilityHandler = nil
   122→
   123→                Task {
   124→                    let output = await buffer.getString()
   125→                    let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
   126→                    let errorOutput = String(data: errorData, encoding: .utf8) ?? ""
   127→
   128→                    if process.terminationStatus != 0 && !output.contains("TRANSCRIPT_START") {
   129→                        let errorMsg = errorOutput.isEmpty ? "Process exited with code \(process.terminationStatus)" : errorOutput
   130→                        safeResume(with: .failure(ProcessError.executionFailed(errorMsg.truncated(to: 500))))
   131→                    } else {
   132→                        safeResume(with: .success(output))
   133→                    }
   134→                }
   135→            }
   136→
   137→            do {
   138→                try process.run()
   139→            } catch {
   140→                timeoutTimer.cancel()
   141→                safeResume(with: .failure(error))
   142→            }
   143→        }
   144→    }
   145→
   146→    /// Runs a simple Python script without progress tracking
   147→    /// - Parameter script: The Python script to execute
   148→    /// - Returns: The output from the script
   149→    static func runPython(script: String) async throws -> String {
   150→        let process = Process()
   151→        let outputPipe = Pipe()
   152→        let errorPipe = Pipe()
   153→
   154→        // Use arch -arm64 to ensure native ARM execution
   155→        process.executableURL = URL(fileURLWithPath: "/usr/bin/arch")
   156→        process.arguments = ["-arm64", AppConfig.Executables.pythonPath, "-c", script]
   157→        process.standardOutput = outputPipe
   158→        process.standardError = errorPipe
   159→
   160→        try process.run()
   161→        process.waitUntilExit()
   162→
   163→        let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
   164→        let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
   165→
   166→        if process.terminationStatus != 0 {
   167→            let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error"
   168→            throw ProcessError.executionFailed(errorString.truncated(to: 500))
   169→        }
   170→
   171→        guard let output = String(data: outputData, encoding: .utf8) else {
   172→            throw ProcessError.invalidOutput
   173→        }
   174→
   175→        return output
   176→    }
   177→}
   178→
   179→// MARK: - String Extensions
   180→
   181→extension String {
   182→    /// Truncates string to specified length with ellipsis
   183→    func truncated(to length: Int, trailing: String = "...") -> String {
   184→        if self.count <= length {
   185→            return self
   186→        }
   187→        return String(self.prefix(length - trailing.count)) + trailing
   188→    }
   189→
   190→    /// Escapes single quotes for Python string interpolation
   191→    var pythonEscaped: String {
   192→        self.replacingOccurrences(of: "'", with: "\\'")
   193→            .replacingOccurrences(of: "\\", with: "\\\\")
   194→    }
   195→}
   196→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

// MARK: - Process Utilities

/// Shared utilities for running Python scripts with progress tracking
enum ProcessUtils {

    // MARK: - Errors

    enum ProcessError: Error, LocalizedError {
        case executionFailed(String)
        case timeout
        case invalidOutput

        var errorDescription: String? {
            switch self {
            case .executionFailed(let message):
                return "Process execution failed: \(message)"
            case .timeout:
                return "Process timed out"
            case .invalidOutput:
                return "Invalid process output"
            }
        }
    }

    // MARK: - Thread-Safe Buffer

    /// Thread-safe actor for collecting process output data
    actor ProcessDataBuffer {
        private var data = Data()

        func append(_ newData: Data) {
            data.append(newData)
        }

        func getData() -> Data {
            return data
        }

        func getString() -> String {
            return String(data: data, encoding: .utf8) ?? ""
        }

        func clear() {
            data = Data()
        }
    }

    // MARK: - Python Execution

    /// Runs a Python script with real-time progress updates
    /// - Parameters:
    ///   - script: The Python script to execute
    ///   - timeout: Maximum execution time in seconds (default: 1 hour)
    ///   - progress: Callback for progress updates (lines starting with "PROGRESS: ")
    /// - Returns: The complete output from the script
    static func runPythonWithProgress(
        script: String,
        timeout: TimeInterval = 3600,
        progress: @escaping @Sendable (String) -> Void
    ) async throws -> String {
        return try await withCheckedThrowingContinuation { continuation in
            let process = Process()
            // Use arch -arm64 to ensure native ARM execution
            process.executableURL = URL(fileURLWithPath: "/usr/bin/arch")
            process.arguments = ["-arm64", AppConfig.Executables.pythonPath, "-c", script]

            let outputPipe = Pipe()
            let errorPipe = Pipe()
            process.standardOutput = outputPipe
            process.standardError = errorPipe

            let buffer = ProcessDataBuffer()
            var hasResumed = false
            let resumeLock = NSLock()

            // Safe resume helper to prevent double-resume
            @Sendable func safeResume(with result: Result<String, Error>) {
                resumeLock.lock()
                defer { resumeLock.unlock() }
                guard !hasResumed else { return }
                hasResumed = true
                continuation.resume(with: result)
            }

            // Read output in real-time
            outputPipe.fileHandleForReading.readabilityHandler = { handle in
                let data = handle.availableData
                if !data.isEmpty {
                    Task {
                        await buffer.append(data)
                    }

                    if let text = String(data: data, encoding: .utf8) {
                        let lines = text.components(separatedBy: "\n")
                        for line in lines {
                            if line.hasPrefix("PROGRESS: ") {
                                let progressMsg = String(line.dropFirst(10))
                                Task { @MainActor in
                                    progress(progressMsg)
                                }
                            }
                        }
                    }
                }
            }

            // Timeout handling
            let timeoutTimer = DispatchSource.makeTimerSource(queue: .global())
            timeoutTimer.schedule(deadline: .now() + timeout)
            timeoutTimer.setEventHandler {
                process.terminate()
                safeResume(with: .failure(ProcessError.timeout))
            }
            timeoutTimer.resume()

            // Process termination handler
            process.terminationHandler = { _ in
                timeoutTimer.cancel()
                outputPipe.fileHandleForReading.readabilityHandler = nil

                Task {
                    let output = await buffer.getString()
                    let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
                    let errorOutput = String(data: errorData, encoding: .utf8) ?? ""

                    if process.terminationStatus != 0 && !output.contains("TRANSCRIPT_START") {
                        let errorMsg = errorOutput.isEmpty ? "Process exited with code \(process.terminationStatus)" : errorOutput
                        safeResume(with: .failure(ProcessError.executionFailed(errorMsg.truncated(to: 500))))
                    } else {
                        safeResume(with: .success(output))
                    }
                }
            }

            do {
                try process.run()
            } catch {
                timeoutTimer.cancel()
                safeResume(with: .failure(error))
            }
        }
    }

    /// Runs a simple Python script without progress tracking
    /// - Parameter script: The Python script to execute
    /// - Returns: The output from the script
    static func runPython(script: String) async throws -> String {
        let process = Process()
        let outputPipe = Pipe()
        let errorPipe = Pipe()

        // Use arch -arm64 to ensure native ARM execution
        process.executableURL = URL(fileURLWithPath: "/usr/bin/arch")
        process.arguments = ["-arm64", AppConfig.Executables.pythonPath, "-c", script]
        process.standardOutput = outputPipe
        process.standardError = errorPipe

        try process.run()
        process.waitUntilExit()

        let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
        let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()

        if process.terminationStatus != 0 {
            let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error"
            throw ProcessError.executionFailed(errorString.truncated(to: 500))
        }

        guard let output = String(data: outputData, encoding: .utf8) else {
            throw ProcessError.invalidOutput
        }

        return output
    }
}

// MARK: - String Extensions

extension String {
    /// Truncates string to specified length with ellipsis
    func truncated(to length: Int, trailing: String = "...") -> String {
        if self.count <= length {
            return self
        }
        return String(self.prefix(length - trailing.count)) + trailing
    }

    /// Escapes single quotes for Python string interpolation
    var pythonEscaped: String {
        self.replacingOccurrences(of: "'", with: "\\'")
            .replacingOccurrences(of: "\\", with: "\\\\")
    }
}


Now let me check the AppConfiguration file to see how paths and dependencies are managed:


Bash

find "/Users/neog/Apps/macOS/YouTubeTranscripts-macOS" -name "*.plist" -o -name "*.yml" | head -10

Find plist and configuration files


     1→import Foundation
     2→
     3→// MARK: - App Configuration
     4→
     5→/// Centralized configuration for the app
     6→/// All hard-coded values should be defined here
     7→enum AppConfig {
     8→
     9→    // MARK: - App Info
    10→
    11→    static let appName = "YouTube Transcripts"
    12→    static let bundleIdentifier = "com.neog.YouTubeTranscripts"
    13→    static let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
    14→    static let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
    15→
    16→    // MARK: - Network
    17→
    18→    enum Network {
    19→        /// Default LM Studio API URL
    20→        static let defaultLMStudioURL = "http://192.168.1.96:1234/v1"
    21→
    22→        /// Network timeouts
    23→        static let shortTimeout: TimeInterval = 10
    24→        static let defaultTimeout: TimeInterval = 30
    25→        static let longTimeout: TimeInterval = 180
    26→        static let transcriptTimeout: TimeInterval = 600 // 10 minutes for long videos
    27→    }
    28→
    29→    // MARK: - YouTube
    30→
    31→    enum YouTube {
    32→        /// Regex patterns for extracting video ID
    33→        static let videoIDPatterns = [
    34→            #"(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})"#,
    35→            #"^([a-zA-Z0-9_-]{11})$"#
    36→        ]
    37→
    38→        /// Supported languages for transcripts
    39→        static let supportedLanguages: [(code: String, name: String)] = [
    40→            ("en", "English"),
    41→            ("pt", "Portuguese"),
    42→            ("es", "Spanish"),
    43→            ("fr", "French"),
    44→            ("de", "German"),
    45→            ("it", "Italian"),
    46→            ("ja", "Japanese"),
    47→            ("ko", "Korean"),
    48→            ("zh", "Chinese")
    49→        ]
    50→
    51→        /// Default transcript language
    52→        static let defaultLanguage = "en"
    53→    }
    54→
    55→    // MARK: - AI
    56→
    57→    enum AI {
    58→        /// Maximum tokens for transcript processing
    59→        static let maxTranscriptTokens = 6000
    60→
    61→        /// Estimated characters per token
    62→        static let charsPerToken = 4
    63→
    64→        /// Maximum characters for transcript (~24000)
    65→        static var maxTranscriptChars: Int { maxTranscriptTokens * charsPerToken }
    66→
    67→        /// Truncation ratio (60% beginning, 40% end)
    68→        static let beginningRatio = 0.6
    69→
    70→        /// Default max tokens for AI response
    71→        static let defaultMaxResponseTokens = 1000
    72→
    73→        /// Temperature for AI generation
    74→        static let defaultTemperature = 0.7
    75→    }
    76→
    77→    // MARK: - Logging
    78→
    79→    enum Logging {
    80→        /// Buffer size before flushing to database
    81→        static let bufferSize = 50
    82→
    83→        /// Maximum recent logs to keep in memory
    84→        static let maxRecentLogs = 500
    85→
    86→        /// Days to keep logs before cleanup
    87→        static let logRetentionDays = 7
    88→
    89→        /// Analytics buffer size
    90→        static let analyticsBufferSize = 20
    91→    }
    92→
    93→    // MARK: - UI
    94→
    95→    enum UI {
    96→        /// Default font size for reading
    97→        static let defaultFontSize: Double = 16
    98→
    99→        /// Font size range
   100→        static let minFontSize: Double = 12
   101→        static let maxFontSize: Double = 24
   102→
   103→        /// Toast display duration
   104→        static let toastDuration: TimeInterval = 2.0
   105→
   106→        /// Auto-refresh interval for diagnostics
   107→        static let diagnosticsRefreshInterval: TimeInterval = 5.0
   108→
   109→        /// Reading speed (words per minute)
   110→        static let readingSpeed = 200
   111→    }
   112→
   113→    // MARK: - Typography
   114→
   115→    enum Typography {
   116→        /// Line spacing multiplier (1.0 = single, 2.0 = double)
   117→        static let defaultLineSpacing: Double = 1.6
   118→        static let minLineSpacing: Double = 1.0
   119→        static let maxLineSpacing: Double = 2.5
   120→
   121→        /// Letter spacing (tracking) in points
   122→        static let defaultLetterSpacing: Double = 0
   123→        static let minLetterSpacing: Double = 0
   124→        static let maxLetterSpacing: Double = 2.0
   125→
   126→        /// Default font family
   127→        static let defaultFontFamily: String = "system"
   128→
   129→        /// Text width as percentage of container
   130→        static let defaultTextWidth: Double = 100
   131→        static let minTextWidth: Double = 60
   132→        static let maxTextWidth: Double = 100
   133→
   134→        /// Paragraph spacing in points
   135→        static let defaultParagraphSpacing: Double = 16
   136→        static let minParagraphSpacing: Double = 8
   137→        static let maxParagraphSpacing: Double = 32
   138→    }
   139→
   140→    // MARK: - External Executables
   141→
   142→    enum Executables {
   143→        /// Python executable path (virtualenv with whisper)
   144→        static var pythonPath: String {
   145→            // Try virtualenv first, then system Python
   146→            let virtualenvPath = "\(NSHomeDirectory())/whisper-arm64-env/bin/python3"
   147→            if FileManager.default.fileExists(atPath: virtualenvPath) {
   148→                return virtualenvPath
   149→            }
   150→            return "/usr/bin/python3"
   151→        }
   152→
   153→        /// FFmpeg executable path
   154→        static var ffmpegPath: String {
   155→            let paths = [
   156→                "\(NSHomeDirectory())/bin/ffmpeg",
   157→                "/opt/homebrew/bin/ffmpeg",
   158→                "/usr/local/bin/ffmpeg",
   159→                "/usr/bin/ffmpeg"
   160→            ]
   161→            for path in paths {
   162→                if FileManager.default.fileExists(atPath: path) {
   163→                    return path
   164→                }
   165→            }
   166→            return "ffmpeg" // Fallback to PATH
   167→        }
   168→
   169→        /// Directory containing ffmpeg (for PATH)
   170→        static var ffmpegDirectory: String {
   171→            let path = ffmpegPath
   172→            return (path as NSString).deletingLastPathComponent
   173→        }
   174→    }
   175→
   176→    // MARK: - Storage
   177→
   178→    enum Storage {
   179→        /// App support directory name
   180→        static let appDirectoryName = "YouTubeTranscripts"
   181→
   182→        /// Database filenames
   183→        static let logsDatabase = "app_logs.sqlite"
   184→        static let analyticsDatabase = "analytics.sqlite"
   185→        static let userProfileFile = "user_profile.json"
   186→
   187→        /// SwiftData store filename
   188→        static let mainStore = "YouTubeTranscripts.store"
   189→    }
   190→
   191→    // MARK: - UserDefaults Keys
   192→
   193→    enum UserDefaultsKeys {
   194→        static let defaultLanguage = "defaultLanguage"
   195→        static let includeTimestamps = "includeTimestamps"
   196→        static let readingFontSize = "readingFontSize"
   197→        static let autoCheckClipboard = "autoCheckClipboard"
   198→        static let lmStudioURL = "lmStudioURL"
   199→        static let defaultSummaryStyle = "defaultSummaryStyle"
   200→
   201→        // AI Provider settings
   202→        static let aiProvider = "aiProvider"
   203→        static let claudeAPIKey = "claudeAPIKey"
   204→        static let claudeModel = "claudeModel"
   205→
   206→        // Typography settings
   207→        static let lineSpacing = "lineSpacing"
   208→        static let letterSpacing = "letterSpacing"
   209→        static let fontFamily = "fontFamily"
   210→        static let textWidth = "textWidth"
   211→        static let paragraphSpacing = "paragraphSpacing"
   212→    }
   213→}
   214→
   215→// MARK: - File Paths
   216→
   217→extension AppConfig {
   218→    /// Get app support directory URL
   219→    static var appSupportDirectory: URL {
   220→        let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
   221→        return appSupport.appendingPathComponent(Storage.appDirectoryName, isDirectory: true)
   222→    }
   223→
   224→    /// Get logs database URL
   225→    static var logsDBURL: URL {
   226→        appSupportDirectory.appendingPathComponent(Storage.logsDatabase)
   227→    }
   228→
   229→    /// Get analytics database URL
   230→    static var analyticsDBURL: URL {
   231→        appSupportDirectory.appendingPathComponent(Storage.analyticsDatabase)
   232→    }
   233→
   234→    /// Get user profile URL
   235→    static var userProfileURL: URL {
   236→        appSupportDirectory.appendingPathComponent(Storage.userProfileFile)
   237→    }
   238→
   239→    /// Ensure app directory exists
   240→    static func ensureAppDirectoryExists() throws {
   241→        try FileManager.default.createDirectory(
   242→            at: appSupportDirectory,
   243→            withIntermediateDirectories: true
   244→        )
   245→    }
   246→}
   247→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

// MARK: - App Configuration

/// Centralized configuration for the app
/// All hard-coded values should be defined here
enum AppConfig {

    // MARK: - App Info

    static let appName = "YouTube Transcripts"
    static let bundleIdentifier = "com.neog.YouTubeTranscripts"
    static let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
    static let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"

    // MARK: - Network

    enum Network {
        /// Default LM Studio API URL
        static let defaultLMStudioURL = "http://192.168.1.96:1234/v1"

        /// Network timeouts
        static let shortTimeout: TimeInterval = 10
        static let defaultTimeout: TimeInterval = 30
        static let longTimeout: TimeInterval = 180
        static let transcriptTimeout: TimeInterval = 600 // 10 minutes for long videos
    }

    // MARK: - YouTube

    enum YouTube {
        /// Regex patterns for extracting video ID
        static let videoIDPatterns = [
            #"(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})"#,
            #"^([a-zA-Z0-9_-]{11})$"#
        ]

        /// Supported languages for transcripts
        static let supportedLanguages: [(code: String, name: String)] = [
            ("en", "English"),
            ("pt", "Portuguese"),
            ("es", "Spanish"),
            ("fr", "French"),
            ("de", "German"),
            ("it", "Italian"),
            ("ja", "Japanese"),
            ("ko", "Korean"),
            ("zh", "Chinese")
        ]

        /// Default transcript language
        static let defaultLanguage = "en"
    }

    // MARK: - AI

    enum AI {
        /// Maximum tokens for transcript processing
        static let maxTranscriptTokens = 6000

        /// Estimated characters per token
        static let charsPerToken = 4

        /// Maximum characters for transcript (~24000)
        static var maxTranscriptChars: Int { maxTranscriptTokens * charsPerToken }

        /// Truncation ratio (60% beginning, 40% end)
        static let beginningRatio = 0.6

        /// Default max tokens for AI response
        static let defaultMaxResponseTokens = 1000

        /// Temperature for AI generation
        static let defaultTemperature = 0.7
    }

    // MARK: - Logging

    enum Logging {
        /// Buffer size before flushing to database
        static let bufferSize = 50

        /// Maximum recent logs to keep in memory
        static let maxRecentLogs = 500

        /// Days to keep logs before cleanup
        static let logRetentionDays = 7

        /// Analytics buffer size
        static let analyticsBufferSize = 20
    }

    // MARK: - UI

    enum UI {
        /// Default font size for reading
        static let defaultFontSize: Double = 16

        /// Font size range
        static let minFontSize: Double = 12
        static let maxFontSize: Double = 24

        /// Toast display duration
        static let toastDuration: TimeInterval = 2.0

        /// Auto-refresh interval for diagnostics
        static let diagnosticsRefreshInterval: TimeInterval = 5.0

        /// Reading speed (words per minute)
        static let readingSpeed = 200
    }

    // MARK: - Typography

    enum Typography {
        /// Line spacing multiplier (1.0 = single, 2.0 = double)
        static let defaultLineSpacing: Double = 1.6
        static let minLineSpacing: Double = 1.0
        static let maxLineSpacing: Double = 2.5

        /// Letter spacing (tracking) in points
        static let defaultLetterSpacing: Double = 0
        static let minLetterSpacing: Double = 0
        static let maxLetterSpacing: Double = 2.0

        /// Default font family
        static let defaultFontFamily: String = "system"

        /// Text width as percentage of container
        static let defaultTextWidth: Double = 100
        static let minTextWidth: Double = 60
        static let maxTextWidth: Double = 100

        /// Paragraph spacing in points
        static let defaultParagraphSpacing: Double = 16
        static let minParagraphSpacing: Double = 8
        static let maxParagraphSpacing: Double = 32
    }

    // MARK: - External Executables

    enum Executables {
        /// Python executable path (virtualenv with whisper)
        static var pythonPath: String {
            // Try virtualenv first, then system Python
            let virtualenvPath = "\(NSHomeDirectory())/whisper-arm64-env/bin/python3"
            if FileManager.default.fileExists(atPath: virtualenvPath) {
                return virtualenvPath
            }
            return "/usr/bin/python3"
        }

        /// FFmpeg executable path
        static var ffmpegPath: String {
            let paths = [
                "\(NSHomeDirectory())/bin/ffmpeg",
                "/opt/homebrew/bin/ffmpeg",
                "/usr/local/bin/ffmpeg",
                "/usr/bin/ffmpeg"
            ]
            for path in paths {
                if FileManager.default.fileExists(atPath: path) {
                    return path
                }
            }
            return "ffmpeg" // Fallback to PATH
        }

        /// Directory containing ffmpeg (for PATH)
        static var ffmpegDirectory: String {
            let path = ffmpegPath
            return (path as NSString).deletingLastPathComponent
        }
    }

    // MARK: - Storage

    enum Storage {
        /// App support directory name
        static let appDirectoryName = "YouTubeTranscripts"

        /// Database filenames
        static let logsDatabase = "app_logs.sqlite"
        static let analyticsDatabase = "analytics.sqlite"
        static let userProfileFile = "user_profile.json"

        /// SwiftData store filename
        static let mainStore = "YouTubeTranscripts.store"
    }

    // MARK: - UserDefaults Keys

    enum UserDefaultsKeys {
        static let defaultLanguage = "defaultLanguage"
        static let includeTimestamps = "includeTimestamps"
        static let readingFontSize = "readingFontSize"
        static let autoCheckClipboard = "autoCheckClipboard"
        static let lmStudioURL = "lmStudioURL"
        static let defaultSummaryStyle = "defaultSummaryStyle"

        // AI Provider settings
        static let aiProvider = "aiProvider"
        static let claudeAPIKey = "claudeAPIKey"
        static let claudeModel = "claudeModel"

        // Typography settings
        static let lineSpacing = "lineSpacing"
        static let letterSpacing = "letterSpacing"
        static let fontFamily = "fontFamily"
        static let textWidth = "textWidth"
        static let paragraphSpacing = "paragraphSpacing"
    }
}

// MARK: - File Paths

extension AppConfig {
    /// Get app support directory URL
    static var appSupportDirectory: URL {
        let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
        return appSupport.appendingPathComponent(Storage.appDirectoryName, isDirectory: true)
    }

    /// Get logs database URL
    static var logsDBURL: URL {
        appSupportDirectory.appendingPathComponent(Storage.logsDatabase)
    }

    /// Get analytics database URL
    static var analyticsDBURL: URL {
        appSupportDirectory.appendingPathComponent(Storage.analyticsDatabase)
    }

    /// Get user profile URL
    static var userProfileURL: URL {
        appSupportDirectory.appendingPathComponent(Storage.userProfileFile)
    }

    /// Ensure app directory exists
    static func ensureAppDirectoryExists() throws {
        try FileManager.default.createDirectory(
            at: appSupportDirectory,
            withIntermediateDirectories: true
        )
    }
}


Let me check the project.yml file and look for any signing/provisioning configuration:


grep -r "PROVISIONING_PROFILE\|TEAM_ID\|CODE_SIGN_IDENTITY\|DEVELOPMENT_TEAM" "/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/YouTubeTranscripts.xcodeproj/project.pbxproj" 2>/dev/null | head -20

Check for signing and provisioning configuration


     1→name: YouTubeTranscripts
     2→options:
     3→  bundleIdPrefix: com.neog
     4→  deploymentTarget:
     5→    macOS: "14.0"
     6→  xcodeVersion: "16.0"
     7→  generateEmptyDirectories: true
     8→
     9→settings:
    10→  base:
    11→    SWIFT_VERSION: "5.9"
    12→    MACOSX_DEPLOYMENT_TARGET: "14.0"
    13→    DEVELOPMENT_TEAM: ""
    14→
    15→targets:
    16→  YouTubeTranscripts:
    17→    type: application
    18→    platform: macOS
    19→    sources:
    20→      - Sources
    21→    settings:
    22→      base:
    23→        PRODUCT_BUNDLE_IDENTIFIER: com.neog.YouTubeTranscripts
    24→        INFOPLIST_FILE: Sources/Info.plist
    25→        PRODUCT_NAME: YouTube Transcripts
    26→        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
    27→        ENABLE_HARDENED_RUNTIME: YES
    28→        CODE_SIGN_ENTITLEMENTS: Sources/YouTubeTranscripts.entitlements
    29→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

name: YouTubeTranscripts
options:
  bundleIdPrefix: com.neog
  deploymentTarget:
    macOS: "14.0"
  xcodeVersion: "16.0"
  generateEmptyDirectories: true

settings:
  base:
    SWIFT_VERSION: "5.9"
    MACOSX_DEPLOYMENT_TARGET: "14.0"
    DEVELOPMENT_TEAM: ""

targets:
  YouTubeTranscripts:
    type: application
    platform: macOS
    sources:
      - Sources
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.neog.YouTubeTranscripts
        INFOPLIST_FILE: Sources/Info.plist
        PRODUCT_NAME: YouTube Transcripts
        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
        ENABLE_HARDENED_RUNTIME: YES
        CODE_SIGN_ENTITLEMENTS: Sources/YouTubeTranscripts.entitlements


/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/YouTubeTranscripts.xcodeproj/project.pbxproj:				DEVELOPMENT_TEAM = "";
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/YouTubeTranscripts.xcodeproj/project.pbxproj:				DEVELOPMENT_TEAM = "";


Let me check for app icons and examine the build configuration further:


ls -la "/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Assets.xcassets/" 2>/dev/null

Check for app icon assets


grep -r "PRODUCT_BUNDLE_IDENTIFIER\|CFBundleIdentifier\|CFBundleVersion\|ENABLE_HARDENED_RUNTIME\|CODE_SIGN_ENTITLEMENTS" "/Users/neog/Apps/macOS/YouTubeTranscripts-macOS" --include="*.plist" --include="*.yml" 2>/dev/null

Check bundle identifiers and runtime settings


total 8
drwxr-xr-x@  5 neog  staff  160 Dec  3 02:21 .
drwxr-xr-x@ 11 neog  staff  352 Dec  3 16:22 ..
drwxr-xr-x@  3 neog  staff   96 Dec  3 14:47 AccentColor.colorset
drwxr-xr-x@  3 neog  staff   96 Dec  3 01:43 AppIcon.appiconset
-rw-------@  1 neog  staff   63 Dec  3 01:43 Contents.json


/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/.build/x86_64-apple-macosx/debug/YouTubeTranscripts.dSYM/Contents/Info.plist:		<key>CFBundleIdentifier</key>
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/.build/x86_64-apple-macosx/debug/YouTubeTranscripts.dSYM/Contents/Info.plist:		<key>CFBundleVersion</key>
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/.build/arm64-apple-macosx/debug/YouTubeTranscripts.dSYM/Contents/Info.plist:		<key>CFBundleIdentifier</key>
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/.build/arm64-apple-macosx/debug/YouTubeTranscripts.dSYM/Contents/Info.plist:		<key>CFBundleVersion</key>
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/project.yml:        PRODUCT_BUNDLE_IDENTIFIER: com.neog.YouTubeTranscripts
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/project.yml:        ENABLE_HARDENED_RUNTIME: YES
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/project.yml:        CODE_SIGN_ENTITLEMENTS: Sources/YouTubeTranscripts.entitlements
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Info.plist:    <key>CFBundleIdentifier</key>
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Info.plist:    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Info.plist:    <key>CFBundleVersion</key>


Let me check the AppIcon contents:


List app icon files


     1→{
     2→  "images" : [
     3→    {
     4→      "idiom" : "mac",
     5→      "scale" : "1x",
     6→      "size" : "16x16"
     7→    },
     8→    {
     9→      "idiom" : "mac",
    10→      "scale" : "2x",
    11→      "size" : "16x16"
    12→    },
    13→    {
    14→      "idiom" : "mac",
    15→      "scale" : "1x",
    16→      "size" : "32x32"
    17→    },
    18→    {
    19→      "idiom" : "mac",
    20→      "scale" : "2x",
    21→      "size" : "32x32"
    22→    },
    23→    {
    24→      "idiom" : "mac",
    25→      "scale" : "1x",
    26→      "size" : "128x128"
    27→    },
    28→    {
    29→      "idiom" : "mac",
    30→      "scale" : "2x",
    31→      "size" : "128x128"
    32→    },
    33→    {
    34→      "idiom" : "mac",
    35→      "scale" : "1x",
    36→      "size" : "256x256"
    37→    },
    38→    {
    39→      "idiom" : "mac",
    40→      "scale" : "2x",
    41→      "size" : "256x256"
    42→    },
    43→    {
    44→      "idiom" : "mac",
    45→      "scale" : "1x",
    46→      "size" : "512x512"
    47→    },
    48→    {
    49→      "idiom" : "mac",
    50→      "scale" : "2x",
    51→      "size" : "512x512"
    52→    }
    53→  ],
    54→  "info" : {
    55→    "author" : "xcode",
    56→    "version" : 1
    57→  }
    58→}
    59→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


total 8
drwxr-xr-x@ 3 neog  staff   96 Dec  3 01:43 .
drwxr-xr-x@ 5 neog  staff  160 Dec  3 02:21 ..
-rw-------@ 1 neog  staff  904 Dec  3 01:43 Contents.json


Let me check the build status and look for any other critical files:


grep -r "import Foundation\|Foundation" "/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Services/MCPService.swift" | head -5

Check MCPService imports


     1→import Foundation
     2→
     3→actor MCPService {
     4→    static let shared = MCPService()
     5→
     6→    private init() {}
     7→
     8→    // MARK: - Timeout Configuration
     9→
    10→    /// Default timeout for transcript fetching
    11→    static var defaultTimeout: TimeInterval { AppConfig.Network.transcriptTimeout }
    12→
    13→    // MARK: - Public Methods
    14→
    15→    func getVideoInfo(url: String) async throws -> VideoInfoResponse {
    16→        guard let videoID = MCPService.extractVideoID(from: url) else {
    17→            await AppLogger.shared.warning("Invalid YouTube URL: \(url)", category: .transcript)
    18→            throw MCPError.invalidURL
    19→        }
    20→
    21→        await AppLogger.shared.info("Fetching video info for: \(videoID)", category: .transcript)
    22→
    23→        let script = """
    24→        import json
    25→        from urllib.request import urlopen
    26→        from urllib.parse import urlencode
    27→
    28→        video_id = '\(videoID)'
    29→        url = f'https://www.youtube.com/oembed?{urlencode({"url": f"https://www.youtube.com/watch?v={video_id}", "format": "json"})}'
    30→
    31→        try:
    32→            response = urlopen(url)
    33→            data = json.loads(response.read().decode())
    34→            result = {
    35→                "title": data.get("title", ""),
    36→                "author": data.get("author_name", ""),
    37→                "thumbnail_url": data.get("thumbnail_url", "")
    38→            }
    39→            print(json.dumps(result))
    40→        except Exception as e:
    41→            print(json.dumps({"error": str(e)}))
    42→        """
    43→
    44→        let output = try await runPython(script: script)
    45→        guard let data = output.data(using: .utf8) else {
    46→            throw MCPError.invalidResponse
    47→        }
    48→
    49→        return try JSONDecoder().decode(VideoInfoResponse.self, from: data)
    50→    }
    51→
    52→    func getTranscript(url: String, language: String = "en") async throws -> String {
    53→        guard let videoID = MCPService.extractVideoID(from: url) else {
    54→            await AppLogger.shared.warning("Invalid YouTube URL for transcript: \(url)", category: .transcript)
    55→            throw MCPError.invalidURL
    56→        }
    57→
    58→        await AppLogger.shared.info("Fetching transcript for \(videoID) in \(language)", category: .transcript)
    59→        startTimedEvent("transcript_fetch_\(videoID)")
    60→
    61→        let script = """
    62→        from youtube_transcript_api import YouTubeTranscriptApi
    63→
    64→        video_id = '\(videoID)'
    65→        lang = '\(language)'
    66→
    67→        try:
    68→            ytt_api = YouTubeTranscriptApi()
    69→            transcript = ytt_api.fetch(video_id, languages=[lang, 'en'])
    70→            full_text = ' '.join([item.text for item in transcript])
    71→            print(full_text)
    72→        except Exception as e:
    73→            print(f"ERROR: {str(e)}")
    74→        """
    75→
    76→        let output = try await runPython(script: script)
    77→
    78→        if output.hasPrefix("ERROR:") {
    79→            throw MCPError.transcriptNotAvailable(output)
    80→        }
    81→
    82→        return output.trimmingCharacters(in: .whitespacesAndNewlines)
    83→    }
    84→
    85→    func getTimedTranscript(url: String, language: String = "en") async throws -> String {
    86→        guard let videoID = MCPService.extractVideoID(from: url) else {
    87→            throw MCPError.invalidURL
    88→        }
    89→
    90→        let script = """
    91→        from youtube_transcript_api import YouTubeTranscriptApi
    92→
    93→        video_id = '\(videoID)'
    94→        lang = '\(language)'
    95→
    96→        def format_time(seconds):
    97→            mins = int(seconds // 60)
    98→            secs = int(seconds % 60)
    99→            return f"[{mins:02d}:{secs:02d}]"
   100→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

actor MCPService {
    static let shared = MCPService()

    private init() {}

    // MARK: - Timeout Configuration

    /// Default timeout for transcript fetching
    static var defaultTimeout: TimeInterval { AppConfig.Network.transcriptTimeout }

    // MARK: - Public Methods

    func getVideoInfo(url: String) async throws -> VideoInfoResponse {
        guard let videoID = MCPService.extractVideoID(from: url) else {
            await AppLogger.shared.warning("Invalid YouTube URL: \(url)", category: .transcript)
            throw MCPError.invalidURL
        }

        await AppLogger.shared.info("Fetching video info for: \(videoID)", category: .transcript)

        let script = """
        import json
        from urllib.request import urlopen
        from urllib.parse import urlencode

        video_id = '\(videoID)'
        url = f'https://www.youtube.com/oembed?{urlencode({"url": f"https://www.youtube.com/watch?v={video_id}", "format": "json"})}'

        try:
            response = urlopen(url)
            data = json.loads(response.read().decode())
            result = {
                "title": data.get("title", ""),
                "author": data.get("author_name", ""),
                "thumbnail_url": data.get("thumbnail_url", "")
            }
            print(json.dumps(result))
        except Exception as e:
            print(json.dumps({"error": str(e)}))
        """

        let output = try await runPython(script: script)
        guard let data = output.data(using: .utf8) else {
            throw MCPError.invalidResponse
        }

        return try JSONDecoder().decode(VideoInfoResponse.self, from: data)
    }

    func getTranscript(url: String, language: String = "en") async throws -> String {
        guard let videoID = MCPService.extractVideoID(from: url) else {
            await AppLogger.shared.warning("Invalid YouTube URL for transcript: \(url)", category: .transcript)
            throw MCPError.invalidURL
        }

        await AppLogger.shared.info("Fetching transcript for \(videoID) in \(language)", category: .transcript)
        startTimedEvent("transcript_fetch_\(videoID)")

        let script = """
        from youtube_transcript_api import YouTubeTranscriptApi

        video_id = '\(videoID)'
        lang = '\(language)'

        try:
            ytt_api = YouTubeTranscriptApi()
            transcript = ytt_api.fetch(video_id, languages=[lang, 'en'])
            full_text = ' '.join([item.text for item in transcript])
            print(full_text)
        except Exception as e:
            print(f"ERROR: {str(e)}")
        """

        let output = try await runPython(script: script)

        if output.hasPrefix("ERROR:") {
            throw MCPError.transcriptNotAvailable(output)
        }

        return output.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    func getTimedTranscript(url: String, language: String = "en") async throws -> String {
        guard let videoID = MCPService.extractVideoID(from: url) else {
            throw MCPError.invalidURL
        }

        let script = """
        from youtube_transcript_api import YouTubeTranscriptApi

        video_id = '\(videoID)'
        lang = '\(language)'

        def format_time(seconds):
            mins = int(seconds // 60)
            secs = int(seconds % 60)
            return f"[{mins:02d}:{secs:02d}]"


/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Services/MCPService.swift:import Foundation


Let me check the GitHub information and the build status:


Check gitignore file


cd "/Users/neog/Apps/macOS/YouTubeTranscripts-macOS" && git remote -v 2>/dev/null || echo "No git remote configured"

Check git remote configuration


# Xcode
.DS_Store
build/
DerivedData/
*.xcuserstate
*.xcuserdatad
xcuserdata/
*.xcworkspace/xcuserdata/
*.xcodeproj/xcuserdata/
*.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

# Swift Package Manager
.build/
.swiftpm/
Package.resolved

# CocoaPods
Pods/
Podfile.lock

# Carthage
Carthage/Build/

# Archives
*.xcarchive
*.ipa
*.dSYM.zip
*.dSYM

# Playgrounds
timeline.xctimeline
playground.xcworkspace

# App packaging
*.app
*.framework
*.dylib

# Backup files
*.bak
*.orig
*~

# IDE
.idea/
*.swp
*.swo

# Python
__pycache__/
*.py[cod]
*.pyo
.Python
*.egg-info/
.eggs/
*.egg
.venv/
venv/
ENV/

# Temporary files
*.tmp
*.temp
*.log

# OS files
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db


origin	https://github.com/leandro1416/YouTubeTranscripts-macOS.git (fetch)
origin	https://github.com/leandro1416/YouTubeTranscripts-macOS.git (push)


Perfect! Now let me check the LICENSE file and compile a summary:


cd "/Users/neog/Apps/macOS/YouTubeTranscripts-macOS" && git log --oneline | head -10

Check recent commits


     1→MIT License
     2→
     3→Copyright (c) 2025 leandro1416
     4→
     5→Permission is hereby granted, free of charge, to any person obtaining a copy
     6→of this software and associated documentation files (the "Software"), to deal
     7→in the Software without restriction, including without limitation the rights
     8→to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9→copies of the Software, and to permit persons to whom the Software is
    10→furnished to do so, subject to the following conditions:
    11→
    12→The above copyright notice and this permission notice shall be included in all
    13→copies or substantial portions of the Software.
    14→
    15→THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16→IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17→FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18→AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19→LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20→OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21→SOFTWARE.
    22→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

MIT License

Copyright (c) 2025 leandro1416

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


b05f32d Add MIT License
2e5e8ed Add comprehensive README with setup and usage instructions
fcc1195 Add .gitignore for Xcode/Swift project
648cdfc Fix YouTube audio download with pytubefix fallback


Excellent. Now I have all the information I need. Let me compile a comprehensive report on the TestFlight deployment readiness:

## YouTubeTranscripts-macOS: TestFlight Deployment Report

Based on my exploration of the project, here's a detailed assessment of TestFlight deployment requirements:

### 1. Project Structure & Build Configuration

**Current Setup:**
- Uses Xcode project with Swift Package Manager (Package.swift) coordination
- SwiftUI-based native macOS application
- Swift 5.9, minimum macOS 14.0 (Sonoma) deployment target
- Project generator: XcodeGen (configured in project.yml)
- Licensed under MIT

**Build Status:**
- Project builds successfully (evidenced by .build/ directory with arm64 and x86_64 binaries)
- Uses hardened runtime enabled (ENABLE_HARDENED_RUNTIME: YES)
- Has entitlements configured via YouTubeTranscripts.entitlements

**File Path:** `/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/`

### 2. Bundle & Signing Configuration

**Critical Issue: Bundle Identifier & Team ID Missing**

```
Bundle Identifier: com.neog.YouTubeTranscripts
Current Team ID: "" (EMPTY - project.yml line 13)
```

**Status: NOT READY FOR TESTFLIGHT**

The `DEVELOPMENT_TEAM` is empty in both project.yml and project.pbxproj. This MUST be set before TestFlight submission.

**Required Action:**
- Set DEVELOPMENT_TEAM to Apple Developer Account Team ID (10 character alphanumeric)
- Update in project.yml: `DEVELOPMENT_TEAM: "XXXXXXXXXX"` (replace with actual team ID)

### 3. App Icons

**Status: INCOMPLETE**

- AppIcon.appiconset defined in Contents.json with all required sizes:
  - 16x16 (1x, 2x)
  - 32x32 (1x, 2x)
  - 128x128 (1x, 2x)
  - 256x256 (1x, 2x)
  - 512x512 (1x, 2x)

**Issue:** Icon image files are MISSING from the asset catalog. Only Contents.json exists, no actual .png/.tiff files.

**Required Action:**
- Generate or provide app icons in all required sizes
- Place in: `/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Assets.xcassets/AppIcon.appiconset/`

### 4. Privacy & Entitlements

**Current Entitlements:**
- YouTubeTranscripts.entitlements is empty (contains only `<dict/>`)

**Privacy Description Already Configured:**
- NSLocalNetworkUsageDescription: "YouTube Transcripts needs local network access to connect to LM Studio for AI-powered transcript summaries."
- NSBonjourServices: `_http._tcp` (for local network discovery)

**Additional Privacy Descriptions Needed for TestFlight:**

Based on functionality analysis, you should add:
```xml
<key>NSHumanReadableCopyright</key>
<string>Copyright 2025 NEOG. All rights reserved.</string>

<key>NSLocalNetworkUsageDescription</key>
<string>YouTube Transcripts needs local network access to connect to LM Studio for AI-powered transcript summaries.</string>

<key>NSBonjourServices</key>
<array>
    <string>_http._tcp</string>
</array>
```

Consider adding if used:
- NSMicrophoneUsageDescription (if recording audio)
- NSAppleEventsUsageDescription (if controlling other apps)
- NSUserTrackingUsageDescription (if analytics enabled)

### 5. External Dependencies & Binary Requirements

**CRITICAL: Dependency on System Binaries**

The app requires external Python packages and system utilities that are NOT bundled:

**Python Dependencies (Required on user's Mac):**
```
Primary:
- openai-whisper (speech-to-text transcription)
- pytubefix (YouTube audio downloader)

Fallback:
- yt-dlp (alternative YouTube downloader)
- youtube_transcript_api (caption fetching)
```

**System Binaries:**
```
- Python 3.9+ (/usr/bin/python3)
- ffmpeg (audio conversion - checked in multiple paths:
  - ~/bin/ffmpeg
  - /opt/homebrew/bin/ffmpeg
  - /usr/local/bin/ffmpeg
  - /usr/bin/ffmpeg
- Optional virtualenv: ~/whisper-arm64-env/bin/python3
```

**Problem for TestFlight:**
TestFlight requires standalone, self-contained apps. Users cannot install system dependencies via TestFlight. This architecture WILL FAIL TestFlight submission.

**Solutions:**

Option A (Recommended): Bundle Python & Dependencies
- Use PyInstaller or similar to create standalone Python executable with all dependencies
- Include in app bundle: `YouTubeTranscripts.app/Contents/Resources/whisper/`
- Update AppConfiguration.swift to use bundled paths

Option B: Remove Offline Transcription
- Remove Whisper/pytubefix/yt-dlp functionality
- Rely on YouTube captions only
- Use cloud APIs (OpenAI Whisper API) if transcription needed

Option C: Require Manual Setup
- Not recommended - users cannot install pip packages for sandbox app

### 6. Hardcoded Paths & Network Configuration

**Potential Issues:**

1. **LM Studio Hardcoded IP:**
   ```swift
   static let defaultLMStudioURL = "http://192.168.1.96:1234/v1"  // Machine-specific!
   ```
   - This is hardcoded to user's specific network setup
   - TestFlight reviewers won't have this setup
   - Should default to localhost or be configurable

2. **Python/FFmpeg Path Resolution:**
   - Currently uses PATH searches with fallbacks
   - Works for individual machines but may fail in sandbox environment
   - Better to bundle or use system Python via Foundation

3. **Temporary Files:**
   - Uses `FileManager.default.temporaryDirectory` (correct)
   - Creates `youtube-audio/` subdirectory (acceptable)
   - Cleanup handled after processing

4. **SwiftData Storage:**
   - Stored in Application Support directory (correct)
   - Uses `YouTubeTranscripts.store` filename
   - Should be sandboxed properly

### 7. Code Signing & Sandboxing Requirements

**Current Configuration:**
- Hardened Runtime: YES
- Entitlements file: Empty (needs population)

**Potential Issues for App Sandbox:**

The app needs entitlements for:
```xml
<key>com.apple.security.app-sandbox</key>
<true/>

<key>com.apple.security.files.user-selected.read-write</key>
<true/>

<key>com.apple.security.network.client</key>
<true/>

<key>com.apple.security.network.server</key>
<true/>

<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
    <string>/tmp</string>
    <string>/var/tmp</string>
</array>

<!-- If using Process() for Python/ffmpeg -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>

<key>com.apple.security.cs.disable-library-validation</key>
<true/>
```

### 8. Build & Version Management

**Current Versions:**
```
CFBundleShortVersionString: 1.0
CFBundleVersion: 1
MACOSX_DEPLOYMENT_TARGET: 14.0 (Sonoma+)
Swift Version: 5.9
Xcode Version Required: 16.0
```

**Required for TestFlight:**
- Version numbers should follow semantic versioning
- Build number increments with each submission
- Deployment target should be reasonable (14.0 is good)

### 9. TestFlight Submission Checklist

**BLOCKING ISSUES (Must Fix):**
- [ ] Team ID is empty - set DEVELOPMENT_TEAM before build
- [ ] App icons missing actual image files
- [ ] External Python dependencies not bundled (critical blocker)
- [ ] Hardcoded IP address (192.168.1.96) breaks on other machines

**STRONGLY RECOMMENDED:**
- [ ] Bundle Whisper, pytubefix, yt-dlp, or use cloud APIs
- [ ] Remove/make configurable the LM Studio hardcoded IP
- [ ] Populate entitlements.plist with necessary sandbox permissions
- [ ] Add privacy descriptions for all capabilities used
- [ ] Test on clean Mac without pre-installed dependencies
- [ ] Implement graceful degradation if dependencies missing

**GOOD TO HAVE:**
- [ ] Update version to 1.0.0 (or higher if already released)
- [ ] Add Code Release Notes for TestFlight reviewers
- [ ] Create Privacy Policy if collecting any user data
- [ ] Test all Python subprocess calls in sandbox environment

### 10. Licensing Note

- Project uses MIT License with 2025 copyright by leandro1416
- Should be updated if redistributed under different author/organization

## Summary

**Current Status: NOT TESTFLIGHT READY**

**Estimated Work to Make Ready:**

1. **Critical Path (Required):**
   - Set Team ID: 10 minutes
   - Bundle Python/dependencies or switch to cloud APIs: 4-8 hours
   - Add app icons: 1-2 hours
   - Fix hardcoded LM Studio URL: 30 minutes
   - Test on clean machine: 1-2 hours
   - **Subtotal: 7-12 hours minimum**

2. **Polish/Compliance (Recommended):**
   - Configure entitlements properly: 1 hour
   - Add privacy descriptions: 30 minutes
   - App Store optimizations: 1-2 hours
   - **Subtotal: 2.5-3.5 hours**

**Total Estimated Time: 10-15 hours**

The biggest blocker is the Python/external dependency requirement. macOS App Sandbox does not allow arbitrary Python script execution to system tools, and TestFlight reviewers would reject an app that requires users to manually install dependencies. You'll need to either:

1. Bundle everything (increases app size significantly)
2. Use cloud APIs (adds infrastructure cost)
3. Remove the feature entirely
4. Request special entitlements (unlikely to be granted)

Would you like me to provide detailed guidance on any specific aspect of this deployment process?