Basit Anlatım — Herkes İçin
Bir şarkıya "Çal" dediğinizde arka planda 5 katmanlı bir kontrol mekanizması çalışıyor:
- Kimlik Kontrolü: Giriş yapmış mısın? Çerezlerinden (cookie) bakıyor.
- Üyelik Kontrolü: Premium üye misin? Abonelik bitiş tarihin geçmemiş mi?
- Şarkı Hazırlığı: Şifreli stream URL'si üretiliyor, şifreleme anahtarı ayrıca gönderiliyor.
- Veri Toplama: Kim, ne zaman, hangi cihazdan, kaç saniye dinledi — hepsi kaydediliyor.
- Bellek Yönetimi: Eski şarkının izleri siliniyor, yeni şarkının verisi yükleniyor.
Aşağıdaki diyagram tüm bu sürecin haritası. Mavi = okunan, Yeşil = eklenen, Turuncu = güncellenen, Kırmızı = silinen veri.
Tam Mimari Diyagram — Şarkı Çalma Döngüsü
KULLANICI "ÇAL" BUTONUNA BASIYOR │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ TARAYICI (Client-Side) │ │ │ │ 📖 OKUNAN VERİLER: │ │ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ │ │ localStorage: │ │ │ │ • volume → Ses seviyesi (0-100) │ │ │ │ • muzibu_player_state → Son durum (şarkı, sıra, pozisyon) │ │ │ │ • muzibu_play_context → Nereden çalındı (albüm/playlist/arama) │ │ │ │ • muzibu_device_profile_id → Cihaz profil ID │ │ │ │ • muzibu_device_fingerprint → Cihaz parmak izi │ │ │ │ • muzibu_spot_counter → Reklam sayacı (kaç şarkı dinlendi) │ │ │ │ │ │ │ │ Alpine.js Store: │ │ │ │ • store('favorites').favorites → ["song-123","album-45"] │ │ │ │ • store('player').currentSong → Mevcut şarkı bilgisi │ │ │ │ • store('player').queue → Çalma sırası │ │ │ │ │ │ │ │ Memory: │ │ │ │ • streamUrlCache (Map) → Önceden alınmış stream URL'leri (max 30, 5dk TTL) │ │ │ │ • HlsPool._active → Aktif HLS instance'ları (max 3) │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 🗑️ SİLİNEN VERİLER (Eski Şarkı Temizliği): │ │ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ │ │ • Eski HLS instance → HlsPool.release() (listener kaldır, detach, destroy) │ │ │ │ • Eski Blob URL → URL.revokeObjectURL() (bellek serbest) │ │ │ │ • Eski Audio element src → removeAttribute('src') + load() │ │ │ │ • Eski Howler instance → howl.off() + howl.stop() + howl.unload() │ │ │ │ • Eski Event Listener'lar → onended, onerror, onpause, onstalled kaldırılır │ │ │ │ • Preload verisi → _preloadedNext temizlenir │ │ │ │ • Progress interval → clearInterval(progressInterval) │ │ │ │ • Buffer health interval → clearInterval(_bufferCheckInterval) │ │ │ │ • Play count timer → clearTimeout(playCountTimerId) │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ HTTP/HTTPS ┌──────────────────────────────────────┐ │ │ │ POST /api/stream │ ──────────────→ │ SUNUCU (Server-Side) │ │ │ │ + cookie + CSRF │ │ │ │ │ └──────────┬──────────┘ ◄────────────── │ 1. auth('web') → Session cookie oku │ │ │ │ JSON Response │ 2. isPremium() → DB query: │ │ │ │ │ users.subscription_expires_at │ │ │ │ │ > NOW() mu? │ │ │ │ │ 3. getSong() → Redis cache VEYA DB │ │ │ │ │ 4. SignedUrl üret (HMAC-SHA256) │ │ │ │ │ 5. Response: şifreli URL + meta │ │ │ │ └──────────────────────────────────────┘ │ │ ▼ │ │ 📝 EKLENEN VERİLER (Yeni Şarkı): │ │ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ │ │ Memory (Alpine Store): │ │ │ │ • currentSong = { id, title, cover, duration, album, artist, color_hash } │ │ │ │ • queue[index] güncellenir │ │ │ │ • streamUrlCache.set(songId) → signed URL cache'e eklenir │ │ │ │ • currentPlayId = API'den dönen play_id │ │ │ │ • playbackStartTime = Date.now() │ │ │ │ │ │ │ │ DOM: │ │ │ │ • <audio> element → yeni src + event listener'lar eklenir │ │ │ │ • MediaSession → title, artist, artwork (kilit ekranı) │ │ │ │ • document.title = "Şarkı Adı - Sanatçı | Muzibu" │ │ │ │ • Blob URL → stream URL için yeni blob oluşturulur │ │ │ │ • HLS instance → HlsPool.acquire() ile yeni instance │ │ │ │ │ │ │ │ localStorage: │ │ │ │ • muzibu_player_state → güncel şarkı + sıra + pozisyon kaydedilir │ │ │ │ • muzibu_play_context → { songId, albumId, context, timestamp } │ │ │ │ • muzibu_spot_counter → spot sayacı +1 (30s dinlenirse) │ │ │ │ │ │ │ │ Timer'lar: │ │ │ │ • progressInterval → 250ms aralıkla ilerleme çubuğu güncelleme │ │ │ │ • _bufferCheckInterval → 500ms aralıkla buffer sağlık kontrolü │ │ │ │ • playCountTimerId → 30s sonra play_count artır (setTimeout) │ │ │ │ • queueMonitorInterval → 10s'de bir sıra doluluk kontrolü │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ Şarkı çalmaya başladı, arka planda devam eden işlemler: │ │ │ │ │ ├──→ POST /api/track-start ───→ SUNUCU: INSERT muzibu_song_plays │ │ │ (hemen) { song_id, user_id, device_profile_id, │ │ │ ip_address, source_type, created_at } │ │ │ ◄── { play_id: 12345 } │ │ │ │ │ ├──→ POST /api/track-hit ────→ SUNUCU: UPDATE songs SET play_count+1 │ │ │ (30 saniye sonra) │ │ │ │ │ ├──→ GET /api/songs/next/stream → Sonraki şarkının URL'si (preload) │ │ │ (şarkının %80'inde) │ │ │ │ │ ├──→ HLS segment indirme ───→ Her 3-5 saniyede ~100KB segment │ │ │ (sürekli) │ │ │ │ │ └──→ POST /api/track-end ────→ SUNUCU: UPDATE muzibu_song_plays │ │ (şarkı bitince veya SET ended_at, listened_duration, │ │ geçince veya was_skipped, stop_reason │ │ tab kapatılınca) │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ SUNUCU (Server-Side) — Veritabanı İşlemleri │ │ │ │ 📖 OKUNAN TABLOLAR: │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │ users → id, subscription_expires_at (isPremium check) │ │ │ │ muzibu_songs → song_id, hls_path, duration, encryption_key │ │ │ │ muzibu_albums → album bilgisi (eager load) │ │ │ │ muzibu_artists → sanatçı bilgisi (eager load) │ │ │ │ muzibu_song_plays → duplicate check (son 5 saniye) │ │ │ │ Redis: song:{id} → Song metadata cache (24 saat TTL) │ │ │ │ Redis: popular_songs → Popüler şarkı cache (30 dakika TTL) │ │ │ │ Redis: enc_key_{id} → Şifreleme anahtarı cache (24 saat TTL) │ │ │ │ Dosya: enc.bin → AES-128 şifreleme anahtarı (16 byte) │ │ │ │ Dosya: master.m3u8 → HLS playlist (modifiye edilerek serve edilir) │ │ │ │ Dosya: segment-*.ts → Şifreli ses parçaları │ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ 📝 EKLENEN VERİLER (INSERT): │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │ muzibu_song_plays ← YENİ KAYIT (her şarkı başlangıcında) │ │ │ │ { song_id, user_id, device_profile_id, ip_address, │ │ │ │ source_type, source_id, created_at } │ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ 🔄 GÜNCELLENEN VERİLER (UPDATE): │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │ muzibu_songs.play_count ← +1 INCREMENT (30 saniye sonra) │ │ │ │ muzibu_song_plays ← ended_at, listened_duration, was_skipped, │ │ │ │ stop_reason (şarkı sonunda) │ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ 🗑️ SİLİNEN VERİLER: │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │ Sunucu tarafında hiçbir veri silinmiyor. │ │ │ │ Tüm dinleme kayıtları saklanır (analitik için). │ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────────────────┘
Üyelik & Yetki Kontrol Zinciri
Bir şarkı çalma isteğinde sunucunun yaptığı 4 aşamalı kontrol
Aşama 1: Giriş Yapılmış mı?
READKontrol: auth('web')->user() ?? auth('sanctum')->user()
Okunan: Session cookie (laravel_session) veya Bearer token
Başarısız: 401 Unauthorized
→ Tarayıcıda: Tüm localStorage temizlenir, /login'e yönlendirilir
Aşama 2: Premium Üye mi?
READKontrol: $user->isPremium() → iki katmanlı:
// 1. Model değeri (hızlı, cache'den) if ($this->subscription_expires_at && $this->subscription_expires_at->isFuture()) { return true; } // 2. Fresh DB sorgusu (model stale olabilir) $freshExpiry = DB::table('users') ->where('id', $this->id) ->value('subscription_expires_at'); return $freshExpiry && Carbon::parse($freshExpiry)->isFuture();
Tek Kriter: subscription_expires_at > NOW()
| NULL | = Free kullanıcı → 402 |
| 2025-12-31 (geçmiş) | = Süresi dolmuş → 402 |
| 2026-06-15 (gelecek) | = Aktif premium → devam |
402 Payment Required (Free / Süresi Dolmuş)
{
"status": "subscription_required",
"redirect": "/subscription/plans",
"message": "Müzik dinlemek için premium üyelik gerekli",
"song": { "id": 123, "title": "...", "cover_url": "..." },
"is_premium": false,
"subscription_ends_at": null
}
Stream URL dönmez. Şarkı çalınamaz. Kullanıcı abonelik sayfasına yönlendirilir.
Aşama 3: Şarkı HLS'e Dönüştürülmüş mü?
READKontrol: $song->hls_path dolu mu?
| hls_path dolu | → HLS stream URL üret, status: "ready" |
| hls_path boş | → MP3 fallback URL üret + ConvertToHLSJob dispatch, status: "converting" |
Aşama 4: Signed URL Üretimi
READİmza: HMAC-SHA256(path + userId + expires, APP_KEY)
TTL: Şarkı süresi + 5 dk buffer (min 30dk, max 60dk)
Şifreleme: Stream URL XOR + Base64 ile şifrelenip tarayıcıya gönderilir
Oturum Sonlandırma Akışı (401 / Force Logout)
Tetikleyiciler
- Başka cihazdan giriş
- Session süresi doldu
- CSRF token uyuşmazlığı
- Abonelik iptal
Silinen Veriler (Tarayıcı)
- muzibu_player_state
- muzibu_queue
- muzibu_favorites
- muzibu_play_context
- muzibu_volume
- sessionStorage.clear()
- ServiceWorker cache
Yönlendirme
Hard redirect (SPA bypass):
/login?session_terminated=1
Modal: "Başka bir cihazdan giriş yapıldı"
Şarkı Yaşam Döngüsü — Zaman Çizelgesi
Bir şarkının başlangıcından sonuna kadar yapılan tüm veri işlemleri
T0: Çal Butonuna Basıldı (0ms)
Okunan:
- localStorage: volume, player_state, play_context
- Alpine Store: currentSong, queue, queueIndex
- streamUrlCache: varsa cached URL kullanılır
Silinen (eski şarkı):
- HLS instance → HlsPool.release()
- Blob URL → revokeObjectURL()
- Audio src → removeAttribute('src')
- Howler → .off() + .stop() + .unload()
- Timer'lar → clearInterval/clearTimeout (3 adet)
T1: API Çağrısı — /api/stream (100-400ms)
Sunucuda okunan:
| 1. | Session cookie | → auth('web') kullanıcı kimliği |
| 2. | users.subscription_expires_at | → Premium kontrolü (FRESH DB query) |
| 3. | Redis: song:{id} | → Şarkı metadata cache (miss ise DB) |
| 4. | muzibu_songs | → hls_path, duration, encryption_key |
| 5. | config('app.key') | → HMAC imza için |
T2: Yeni Şarkı Yükleme (150-600ms)
Tarayıcıya Eklenen:
- + HLS instance (HlsPool.acquire)
- + Blob URL (stream URL gizleme)
- + Audio element src + listeners
- + MediaSession metadata (kilit ekranı)
- + document.title güncelleme
- + progressInterval (250ms timer)
- + bufferCheckInterval (500ms timer)
- + playCountTimer (30s setTimeout)
Store/Storage Güncelleme:
- + Alpine: currentSong = yeni şarkı
- + Alpine: isPlaying = true
- + streamUrlCache.set(songId, url)
- + localStorage: muzibu_player_state
- + localStorage: muzibu_play_context
T3: Ses Çıktı + Arka Plan Başladı (300ms-1.2s)
Sunucuya yazılan:
POST /api/track-start → INSERT INTO muzibu_song_plays: song_id = 123 user_id = 456 device_profile_id = 42 (veya NULL) ip_address = "185.x.x.x" source_type = "album" (playlist, genre, search, radio...) source_id = 789 created_at = NOW() ← Response: { play_id: 12345 }
Duplicate koruması: Aynı şarkı + aynı kullanıcı 5 saniye içinde tekrar INSERT yapılmaz.
T4: 30 Saniye Sonra — Play Count (30s)
POST /api/track-hit → UPDATE muzibu_songs SET play_count = play_count + 1 WHERE song_id = 123 + Tarayıcıda: localStorage: muzibu_spot_counter = songsPlayed + 1 (Reklam sayacı artırılır)
T5: Şarkının %80'i — Preload Sonraki (değişken)
Sonraki şarkı için yeni /api/stream çağrısı, yeni HLS instance, yeni blob URL oluşturulur.
Ayrıca reklam (spot) sistemi: shouldPreloadSpot() kontrolü → spot audio preload başlar.
T6: Şarkı Bitti / Geçildi / Tab Kapatıldı
POST /api/track-end → UPDATE muzibu_song_plays SET ended_at = NOW() listened_duration = 187 (saniye) was_skipped = false stop_reason = "ended" WHERE id = 12345 AND user_id = 456 AND ended_at IS NULL stop_reason değerleri: "ended" → Şarkı doğal bitti "next" → Kullanıcı ileri bastı "prev" → Kullanıcı geri bastı "pause_timeout"→ Uzun süre durduruldu "close" → Tab/pencere kapatıldı (sendBeacon)
localStorage — Tam Harita
| Key | Değer | Ne Zaman Yazılır | Ne Zaman Silinir | Yazan Modül |
|---|---|---|---|---|
| volume | 0-100 (integer) | Ses değiştiğinde | Logout | player-core.js |
| muzibu_player_state | JSON: {currentSongId, isPlaying, currentTime, volume, repeatMode, shuffle, queue...} | Her şarkı değişiminde + periyodik | Logout | player-core.js |
| muzibu_queue | JSON: minimal queue (mevcut +-2 + sonraki 20) | Queue değiştiğinde | Logout | player-core.js |
| muzibu_play_context | JSON: {songId, albumId, context, position, timestamp} | Her şarkı başlangıcında | Logout | player-core.js |
| muzibu_favorites | JSON: ["song-123", "album-45", ...] | Sayfa yüklendiğinde (API'den) | Logout | favorites.js |
| muzibu_spot_counter | Integer: Dinlenen şarkı sayısı | Her şarkı 30s dinlendiğinde +1 | Reklam çalınca sıfırlanır | spot-player.js |
| muzibu_spot_counter_settings | JSON: spot ayar versiyonu | Spot settings yüklendiğinde | Versiyon değişince üzerine yazılır | spot-player.js |
| muzibu_device_profile_id | Integer: Backend device profile ID | İlk ziyarette (bir kez) | Cihaz değişirse üzerine yazılır | device-profiler.js |
| muzibu_device_fingerprint | String: base64 hash (32 char) | İlk ziyarette (bir kez) | Cihaz değişirse üzerine yazılır | device-profiler.js |
| remembered_email | String: e-posta (Remember Me) | Login form "Beni Hatırla" | Manuel silme | auth.js |
Bellekte (RAM) Tutulan Veriler
Alpine.js Store Verileri
Gizli State (İzleme)
Veritabanı İşlemleri — Her Şarkı İçin
GET /api/stream — Okunan Veriler
0-2 Query| users | subscription_expires_at (FRESH DB query) |
| Redis: song:{id} | Song metadata (24h cache, miss ise DB'den) |
| muzibu_songs | hls_path, duration, encryption_key, encryption_iv |
| muzibu_albums | album adı, cover (eager load) |
| muzibu_artists | sanatçı adı (eager load) |
POST /api/track-start — Yazılan Veriler
3 Query (1 INSERT)| READ | Song exists check (1 query) |
| READ | Duplicate check: son 5s aynı şarkı (1 query) |
| INSERT | muzibu_song_plays: song_id, user_id, device_profile_id, ip, source_type, created_at |
POST /api/track-hit — Güncellenen (30s sonra)
2 Query (1 UPDATE)| READ | Play ownership check: id + user_id doğrulama |
| UPDATE | muzibu_songs: play_count = play_count + 1 |
POST /api/track-end — Güncellenen (şarkı sonunda)
2 Query (1 UPDATE)| READ | Play check: id + user_id + ended_at IS NULL |
| UPDATE | ended_at, listened_duration, was_skipped, stop_reason |
HLS Dosya Servisi — Okunan
0 Query (dosya)| Dosya | master.m3u8 — Playlist (modifiye: HIGH quality kaldırılıyor, key URL ekleniyor) |
| Dosya | segment-*.ts — Şifreli ses parçaları (direkt serve, immutable cache) |
| Redis/Dosya | enc.bin — AES-128 şifreleme anahtarı (16 byte, Redis 24h cache) |
Toplam DB İşlemi (Tek Şarkı)
Sunucu tarafında hiçbir veri silinmiyor — tüm dinleme kayıtları saklanır.
Her Şarkı Geçişinde Silinen / Temizlenen Veriler
Eski şarkıdan yeni şarkıya geçerken tarayıcıda yapılan temizlik
Bellekten Silinen (RAM)
HlsPool.release() → stopLoad() → detachMedia() → removeAllListeners() → destroy()
~20 MB serbest
URL.revokeObjectURL(_currentBlobUrl)
~1 KB + referans serbest
howl.off('end/load/loaderror') → howl.stop() → howl.unload()
~5-10 MB serbest (MP3 ise)
_preloadedNext = null (preloaded HLS + audio temizlenir)
~20 MB serbest
DOM'dan Silinen
audio.removeAttribute('src') → audio.load()
Element DOM'da kalır ama kaynak serbest
onended, onerror, onpause, onstalled, onwaiting, onseeked → null
6+ listener kaldırılır
clearInterval(progressInterval) — 250ms timer
clearInterval(_bufferCheckInterval) — 500ms timer
clearTimeout(playCountTimerId) — 30s timer
3 timer temizlenir
Kritik Temizlik Sırası
1. HlsPool.release(hls) ← ÖNCE bu (listener'ları kaldır)
2. revokeBlobUrl(blobUrl) ← URL serbest bırak
3. safeAudioCleanup(audio) ← SONRA bu
Ters sıra = audio.load() HLS listener'larını tetikler → internalException!
Çıkış Yapılınca Silinen Her Şey
localStorage
- muzibu_player_state
- muzibu_queue
- muzibu_favorites
- muzibu_play_context
- muzibu_volume
Kalan: device_profile_id, device_fingerprint, remembered_email, spot_counter
sessionStorage
- sessionStorage.clear()
- Tüm session verileri temizlenir
Diğer
- ServiceWorker cache
- Alpine store → reset
- HLS instance'ları → destroy
- Blob URL'ler → revoke
Tam Döngü — 1 Şarkı Hayatı
══════════════════════════════════════════════════════════════════════ 1 ŞARKININ TAM HAYAT DÖNGÜSÜ — NE OKUNUR, NE YAZILIR, NE SİLİNİR ══════════════════════════════════════════════════════════════════════ [T0 — 0ms] KULLANICI "ÇAL" DIYOR │ ├─ READ localStorage.volume → Ses seviyesi al ├─ READ localStorage.muzibu_device_profile_id → Cihaz profili ├─ READ Alpine.store('player').currentSong → Eski şarkı bilgisi ├─ READ streamUrlCache.get(songId) → Varsa cached URL kullan │ ├─ DEL Eski HLS instance → HlsPool.release() [-20MB RAM] ├─ DEL Eski Blob URL → revokeObjectURL() [-1KB] ├─ DEL Eski Audio src + listeners → null + load() [-buffer] ├─ DEL Eski Howler (MP3 ise) → .off().stop().unload()[-10MB] ├─ DEL Eski preload → _preloadedNext=null [-20MB] ├─ DEL progressInterval → clearInterval() ├─ DEL _bufferCheckInterval → clearInterval() ├─ DEL playCountTimerId → clearTimeout() │ ├──→ POST /api/stream/{songId} ──→ SUNUCU │ │ │ ├─ READ Session cookie → Kullanıcı kimliği │ ├─ READ users.subscription_expires_at → Premium mi? (FRESH DB) │ ├─ READ Redis: song:{id} → Cache (24h TTL) │ ├─ READ muzibu_songs + album + artist → Metadata (cache miss) │ ├─ READ config('app.key') → HMAC imzalama │ │ │ └─ Response: { şifreli stream_url, şifreli fallback_url, song metadata } │ [T1 — 100-400ms] API YANIT GELDİ │ ├─ ADD Alpine.currentSong = yeni şarkı → Store güncelleme ├─ ADD streamUrlCache.set(songId, url) → URL cache'e ekle ├─ ADD HlsPool.acquire() → Yeni HLS instance [+20MB] ├─ ADD Blob URL oluştur → Stream URL'yi gizle ├─ ADD Audio element: src + 6 event listener → DOM güncellemesi ├─ ADD MediaSession: title, artist, artwork → Kilit ekranı güncelle ├─ ADD document.title = "Şarkı - Sanatçı" → Sekme başlığı ├─ UPD localStorage.muzibu_player_state → Güncel durum kaydet ├─ UPD localStorage.muzibu_play_context → Yeni context kaydet │ │ ┌─ HLS.js: master.m3u8 indir → enc.bin key al → segment-0.ts indir │ └─ AES-128 decrypt → Audio decode → Ses çıkışı │ [T2 — 300ms-1.2s] SES ÇIKIYOR │ ├─ ADD progressInterval (250ms) → İlerleme çubuğu timer ├─ ADD bufferCheckInterval (500ms) → Buffer sağlık timer ├─ ADD playCountTimerId (30s setTimeout) → Play count timer │ ├──→ POST /api/track-start ──→ SUNUCU │ ├─ READ Duplicate check (son 5s) → Aynı şarkı var mı? │ ├─ INSERT muzibu_song_plays → Yeni dinleme kaydı │ │ { song_id, user_id, device_profile_id, ip, source_type, created_at } │ └─ Response: { play_id: 12345 } │ ├─ ADD currentPlayId = 12345 → RAM'de tut ├─ ADD playbackStartTime = Date.now() → Süre hesabı için │ [T3 — 30 saniye sonra] PLAY COUNT │ ├──→ POST /api/track-hit ──→ SUNUCU │ ├─ READ Play ownership doğrulama → play_id + user_id │ └─ UPDATE muzibu_songs.play_count + 1 → Dinlenme sayısı arttır │ ├─ UPD hitTracked = true → Tekrar gönderme ├─ UPD localStorage.muzibu_spot_counter + 1 → Reklam sayacı arttır │ [T4 — Şarkının %80'i] PRELOAD SONRAKI │ ├──→ GET /api/stream/{nextSongId} ──→ SUNUCU (aynı akış) ├─ ADD _preloadedNext = { hls, audio, ready } → Sonraki hazırla [+20MB] ├─ ADD Yeni Blob URL (preload için) │ │ (Spot sistemi: songsPlayed >= songsBetween ise reklam preload et) ├─ ADD preloadedSpot + preloadedAudio (varsa) [+5MB] │ [T5 — Şarkı Bitti] TRACK END + TEMİZLİK │ ├──→ POST /api/track-end ──→ SUNUCU │ ├─ READ Play check (ended_at IS NULL) │ └─ UPDATE muzibu_song_plays │ SET ended_at = NOW() │ listened_duration = 210 (saniye) │ was_skipped = false │ stop_reason = "ended" | "next" | "prev" | "close" │ ├─ UPD localStorage.muzibu_player_state → Son durum kaydet │ └─ → Sonraki şarkı başlar (T0'a dön) veya durur ══════════════════════════════════════════════════════════════════════ ÖZET: ~10 READ | ~12 WRITE | ~5 UPDATE | ~9 DELETE/CLEANUP DB: 5-7 SELECT | 1 INSERT | 2 UPDATE | 0 DELETE ══════════════════════════════════════════════════════════════════════