MP3 Cloudflare Cache Sistemi

Müzik dosyalarının CDN üzerinden cache'lenmesi

Aktif ve Çalışıyor

📝 Basit Anlatım (Herkes İçin)

Ne Yapıldı?

Müzik dosyaları artık Cloudflare'ın dünya genelindeki sunucularında (CDN) saklanıyor. Kullanıcı bir şarkı çaldığında, dosya en yakın Cloudflare sunucusundan geliyor.

Neden Önemli?

  • Hız: Şarkılar daha hızlı yükleniyor
  • Maliyet: Sunucu bandwidth maliyeti düşüyor
  • Güvenilirlik: Ana sunucu yükü azalıyor
  • Performans: Aynı şarkı tekrar çalındığında anında başlıyor

Nasıl Çalışıyor?

  1. Kullanıcı şarkı çalar
  2. İlk dinlemede dosya ana sunucudan alınır (MISS)
  3. Cloudflare dosyayı 1 yıl boyunca saklar
  4. Sonraki dinlemelerde Cloudflare'den gelir (HIT)

🔧 Teknik Özet

URL Formatı Değişikliği

# Eski (cache'lenmiyordu - Cookie + Query String sorunu)
/api/muzibu/songs/{id}/serve?expires=X&signature=Y
# Yeni (cache'leniyor - HLS ile aynı mantık)
/audio/songs/{id}/{expires}/{signature}

Response Headers

Cache-Control: public, max-age=31536000, immutable
CDN-Cache-Control: public, max-age=31536000
Content-Type: audio/mpeg
Accept-Ranges: bytes
Access-Control-Allow-Origin: *

📄 Değiştirilen Dosyalar ve Kodlar

1. Modules/Muzibu/Providers/MuzibuServiceProvider.php EKLENEN KOD
// 🎵 MP3 AUDIO FILES - Cache-friendly path (no query string, no session)
// Path: /audio/songs/{id}/{expires}/{signature}
// HLS ile aynı mantık: session yok, Cloudflare cache'leyebilir
\Illuminate\Support\Facades\Route::middleware([
    \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class,
    'throttle:120,1', // 120 req/min
])
    ->domain($domain)
    ->get('/audio/songs/{id}/{expires}/{signature}', [
        \Modules\Muzibu\app\Http\Controllers\Api\SongController::class,
        'serveAudio'
    ])
    ->where([
        'id' => '[0-9]+',
        'expires' => '[0-9]+',
        'signature' => '[a-f0-9]+'
    ])
    ->name(($index === 0 ? '' : "d{$index}.") . 'muzibu.songs.audio-serve');
Konum: HLS route tanımından hemen sonra (~satır 320)
2. Modules/Muzibu/App/Http/Controllers/Api/SongController.php YENİ METOD
/**
 * 🎵 Serve MP3 Audio - Cache-friendly endpoint (no query string)
 * Path: /audio/songs/{id}/{expires}/{signature}
 * Cloudflare cache için optimize edilmiş - HLS segment mantığı ile aynı
 *
 * @param int $id
 * @param int $expires Unix timestamp
 * @param string $signature HMAC signature
 * @return \Symfony\Component\HttpFoundation\BinaryFileResponse
 */
public function serveAudio(int $id, int $expires, string $signature)
{
    try {
        // 🔐 İmza doğrulama (path parametrelerinden)
        $expectedSignature = hash_hmac('sha256', "audio|{$id}|{$expires}", config('app.key'));

        if (!hash_equals($expectedSignature, $signature)) {
            \Log::warning('Audio serve: Invalid signature', [
                'song_id' => $id,
                'ip' => request()->ip(),
            ]);
            abort(403, 'Invalid signature');
        }

        // ⏰ Süre kontrolü
        if (time() > $expires) {
            \Log::warning('Audio serve: URL expired', [
                'song_id' => $id,
                'expires' => $expires,
                'now' => time(),
            ]);
            abort(403, 'URL expired');
        }

        // 🎵 Şarkıyı bul
        $song = Song::where('song_id', $id)
            ->where('is_active', 1)
            ->first();

        if (!$song || !$song->file_path) {
            abort(404, 'Song not found');
        }

        // 📁 Dosya yolunu belirle
        if (str_starts_with($song->file_path, '/')) {
            $filePath = $song->file_path;
        } else {
            $filePath = storage_path('app/public/muzibu/songs/' . $song->file_path);
        }

        if (!file_exists($filePath)) {
            \Log::error('Audio serve: File not found', [
                'song_id' => $song->song_id,
                'file_path' => $filePath,
            ]);
            abort(404, 'File not found');
        }

        // 🚀 MP3 döndür - Cloudflare cache için optimize header'lar
        return response()->file($filePath, [
            'Content-Type' => 'audio/mpeg',
            'Accept-Ranges' => 'bytes',
            'Cache-Control' => 'public, max-age=31536000, immutable',
            'CDN-Cache-Control' => 'public, max-age=31536000',
            'Access-Control-Allow-Origin' => '*',
        ]);

    } catch (\Exception $e) {
        \Log::error('Audio serve error:', ['song_id' => $id, 'message' => $e->getMessage()]);
        abort(500, 'Internal error');
    }
}
Konum: serve() metodundan hemen sonra (~satır 343)
3. app/Services/SignedUrlService.php DEĞİŞTİRİLEN METOD
/**
 * Generate signed URL for song stream
 * 🎵 Cache-friendly: /audio/songs/{id}/{expires}/{signature} (no query string)
 *
 * @param int $songId
 * @param int $expiresInMinutes (default: 30 dakika)
 * @param bool $forceMP3 Force MP3 output even if HLS available (for fallback) - DEPRECATED
 * @return string
 */
public function generateStreamUrl(int $songId, int $expiresInMinutes = 30, bool $forceMP3 = false): string
{
    $expiration = Carbon::now()->addMinutes($expiresInMinutes);
    $expires = $expiration->timestamp;

    // 🚀 CACHED: Use TenantHelpers for cached domain lookup
    $tenantDomain = TenantHelpers::getTenantDomain();
    $domain = $tenantDomain ? 'https://' . $tenantDomain : request()->getSchemeAndHttpHost();

    // 🔐 Generate signature (must match SongController::serveAudio)
    // Format: audio|songId|expires → HMAC-SHA256
    $signature = hash_hmac('sha256', "audio|{$songId}|{$expires}", config('app.key'));

    // 🎵 Cache-friendly URL: no query string, Cloudflare can cache
    // Path: /audio/songs/{id}/{expires}/{signature}
    return "{$domain}/audio/songs/{$songId}/{$expires}/{$signature}";
}
Değişiklik: URL formatı query string'den path parametrelerine taşındı
4. app/Http/Middleware/FixResponseCacheHeaders.php EKLENEN KOD
// 🎵 MP3/Audio serve endpoint'leri - Cloudflare cache için (1 yıl)
// Yeni cache-friendly path: /audio/songs/{id}/{expires}/{signature}
// Eski path (backward compat): /api/muzibu/songs/{id}/serve
if ($request->is('audio/songs/*') || $request->is('api/muzibu/songs/*/serve*')) {
    $response->headers->set('Cache-Control', 'public, max-age=31536000, immutable');
    $response->headers->set('CDN-Cache-Control', 'public, max-age=31536000');
    $response->headers->remove('Pragma');
    $response->headers->remove('Expires');
    $response->headers->remove('Set-Cookie'); // Cookie gönderme, cache bozar
    $response->setPublic();
    return $response;
}
Konum: handle() metodunun başında, admin kontrolünden sonra (~satır 42)

☁️ Cloudflare Cache Rule Ayarları

Kural Adı

Muzibu Audio Route Cache (1 Year)

Sıra

1 (First) - ÖNEMLİ!

Expression

(http.request.uri.path contains "/audio/songs/")

Cache Eligibility

Eligible for cache

Edge TTL

Ignore cache-control → 1 year (31536000s)

Browser TTL

Override origin → 1 year

Status

Enabled

Neden /api/ değil /audio/ ?

  • /api/ path'leri Cloudflare'da varsayılan olarak DYNAMIC
  • • Browser /api/ isteklerinde otomatik Cookie gönderiyor
  • • Cloudflare Pro'da Cookie içeren istekler cache'lenmiyor
  • /audio/ path'i bu sorunları bypass ediyor (HLS gibi)

🧪 Test Sunucusu İçin Uygulama Adımları

Adım 1: Kod Değişiklikleri

Yukarıdaki 4 dosyayı güncelle (production'dan kopyala veya git pull)

Modules/Muzibu/Providers/MuzibuServiceProvider.php
Modules/Muzibu/App/Http/Controllers/Api/SongController.php
app/Services/SignedUrlService.php
app/Http/Middleware/FixResponseCacheHeaders.php

Adım 2: Cache Temizle

php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan responsecache:clear

Adım 3: Cloudflare Cache Rule Ekle

  1. Cloudflare Dashboard → domain → Caching → Cache Rules
  2. Create Rule → "Audio Route Cache (1 Year)"
  3. Expression: (http.request.uri.path contains "/audio/songs/")
  4. Cache eligibility: Eligible for cache
  5. Edge TTL: Ignore cache-control → 1 year
  6. Browser TTL: Override origin → 1 year
  7. Sıra: First (en üste al) - ÇOK ÖNEMLİ!

Adım 4: Test Et

  1. Siteye giriş yap
  2. Bir şarkı çal
  3. DevTools → Network → /audio/songs/* filtrele
  4. İlk istek: cf-cache-status: MISS
  5. Aynı şarkıyı tekrar çal
  6. İkinci istek: cf-cache-status: HIT

🔍 Sorun Giderme

cf-cache-status: DYNAMIC

Cloudflare Cache Rule aktif değil veya sıra yanlış. Rule'un "First" konumda olduğundan emin ol. "API No Cache" kuralından ÖNCE olmalı.

403 Forbidden - Invalid signature

İmza doğrulaması başarısız. SignedUrlService ve SongController'daki signature formatının aynı olduğundan emin ol:
hash_hmac('sha256', "audio|{$songId}|{$expires}", config('app.key'))

403 Forbidden - URL expired

URL süresi dolmuş. Varsayılan 30 dakika. Sunucu saati doğru mu kontrol et.

404 Not Found

Route tanınmıyor. php artisan route:clear && php artisan route:list | grep audio ile kontrol et.

Eski URL hâlâ kullanılıyor (/api/muzibu/songs/*/serve)

Cache temizle ve sayfayı yenile. Eski URL'ler 30 dakika sonra expire olur.

✅ Test Sonuçları (21 Şubat 2026)

Şarkı Çalıyor
Cache HIT
Kesinti Yok
Hata Yok

Test Detayları

1. İstek: MISS (origin'den alındı)
2. İstek: HIT (cache'den)
Status: 206 Partial Content
Content-Type: audio/mpeg