DETAYLI UYGULAMA PLANI

Player Limit Sistemi - Kod Planı

Tüm dosyalar, kodlar ve değişiklikler adım adım

Tahmini Süre
~2 Saat

İçindekiler

1 Database & Migration ~15 dakika

database/migrations/tenant/2025_12_17_000001_create_active_playback_sessions_table.php YENİ
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('active_playback_sessions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->uuid('playback_id')->unique();
            $table->string('session_id', 255)->nullable();
            $table->unsignedBigInteger('current_song_id')->nullable();
            $table->string('device_name', 100)->nullable();
            $table->enum('device_type', ['desktop', 'mobile', 'tablet'])->default('desktop');
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->timestamp('last_ping')->useCurrent();
            $table->timestamps();

            // Indexes
            $table->index(['user_id', 'last_ping']);
            $table->index('last_ping');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('active_playback_sessions');
    }
};
database/migrations/tenant/2025_12_17_000002_add_concurrent_streams_limit_to_subscription_plans.php YENİ
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('subscription_plans', function (Blueprint $table) {
            $table->unsignedInteger('concurrent_streams_limit')
                  ->default(1)
                  ->after('device_limit')
                  ->comment('Eşzamanlı aktif playback limiti');
        });
    }

    public function down(): void
    {
        Schema::table('subscription_plans', function (Blueprint $table) {
            $table->dropColumn('concurrent_streams_limit');
        });
    }
};

Migration Komutları

# Tenant migration çalıştır
php artisan tenants:migrate --path=database/migrations/tenant/2025_12_17_000001_create_active_playback_sessions_table.php

php artisan tenants:migrate --path=database/migrations/tenant/2025_12_17_000002_add_concurrent_streams_limit_to_subscription_plans.php

# Veya hepsini birden
php artisan tenants:migrate

2 Backend Service - PlaybackSessionService ~20 dakika

Modules/Muzibu/app/Services/PlaybackSessionService.php YENİ
<?php

namespace Modules\Muzibu\App\Services;

use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Jenssegers\Agent\Agent;

/**
 * PlaybackSessionService - Concurrent Playback Limit Sistemi
 *
 * Spotify modeli: Sınırsız giriş, sınırlı playback
 * Device limit yerine aktif stream limiti uygular
 */
class PlaybackSessionService
{
    protected string $table = 'active_playback_sessions';
    protected int $pingTimeout = 30; // seconds - 3 ping kaçırırsa expired

    /**
     * Servis çalışmalı mı?
     */
    public function shouldRun(): bool
    {
        return tenant() !== null;
    }

    /**
     * Yeni playback session başlat
     * Play butonuna basıldığında çağrılır
     */
    public function registerPlaybackSession(
        User $user,
        string $playbackId,
        ?int $songId = null
    ): array {
        if (!$this->shouldRun()) {
            return ['success' => true, 'playback_id' => $playbackId];
        }

        // 1. Expired session'ları temizle
        $this->cleanupExpiredSessions();

        // 2. Aktif playback sayısını kontrol et
        $activeCount = $this->getActivePlaybackCount($user);
        $limit = $this->getConcurrentStreamLimit($user);

        // 3. Bu playback_id zaten kayıtlı mı? (refresh/reconnect durumu)
        $existingSession = DB::table($this->table)
            ->where('playback_id', $playbackId)
            ->where('user_id', $user->id)
            ->first();

        if ($existingSession) {
            // Mevcut session'ı güncelle
            DB::table($this->table)
                ->where('playback_id', $playbackId)
                ->update([
                    'current_song_id' => $songId,
                    'last_ping' => now(),
                    'updated_at' => now(),
                ]);

            return [
                'success' => true,
                'playback_id' => $playbackId,
                'resumed' => true,
            ];
        }

        // 4. Limit kontrolü (yeni session için)
        if ($activeCount >= $limit) {
            return [
                'success' => false,
                'error' => 'max_streams_exceeded',
                'message' => "Bu abonelik zaten {$activeCount} yerde çalıyor. Limit: {$limit}",
                'active_count' => $activeCount,
                'limit' => $limit,
                'active_sessions' => $this->getActiveSessions($user),
            ];
        }

        // 5. Yeni playback session kaydet
        $agent = new Agent();

        DB::table($this->table)->insert([
            'user_id' => $user->id,
            'playback_id' => $playbackId,
            'session_id' => session()->getId(),
            'current_song_id' => $songId,
            'device_name' => $agent->platform() . ' - ' . $agent->browser(),
            'device_type' => $this->getDeviceType($agent),
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
            'last_ping' => now(),
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        \Log::info('🎵 Playback session started', [
            'user_id' => $user->id,
            'playback_id' => $playbackId,
            'song_id' => $songId,
        ]);

        return [
            'success' => true,
            'playback_id' => $playbackId,
            'active_count' => $activeCount + 1,
            'limit' => $limit,
        ];
    }

    /**
     * Playback heartbeat (10 saniyede bir)
     */
    public function pingPlaybackSession(
        User $user,
        string $playbackId,
        ?int $songId = null
    ): array {
        if (!$this->shouldRun()) {
            return ['success' => true];
        }

        $updated = DB::table($this->table)
            ->where('playback_id', $playbackId)
            ->where('user_id', $user->id)
            ->update([
                'last_ping' => now(),
                'current_song_id' => $songId,
                'updated_at' => now(),
            ]);

        if ($updated === 0) {
            // Session bulunamadı - muhtemelen timeout oldu veya silinmiş
            return [
                'success' => false,
                'error' => 'session_not_found',
                'message' => 'Playback session bulunamadı. Başka bir yerden durdurulmuş olabilir.',
            ];
        }

        return ['success' => true];
    }

    /**
     * Playback session sonlandır (pause/stop)
     */
    public function stopPlaybackSession(User $user, string $playbackId): bool
    {
        if (!$this->shouldRun()) {
            return true;
        }

        $deleted = DB::table($this->table)
            ->where('playback_id', $playbackId)
            ->where('user_id', $user->id)
            ->delete();

        if ($deleted > 0) {
            \Log::info('🎵 Playback session stopped', [
                'user_id' => $user->id,
                'playback_id' => $playbackId,
            ]);
        }

        return $deleted > 0;
    }

    /**
     * Belirli bir session'ı zorla durdur (admin veya kullanıcı isteği)
     */
    public function forceStopSession(User $user, string $playbackId): bool
    {
        return $this->stopPlaybackSession($user, $playbackId);
    }

    /**
     * Tüm playback session'ları durdur
     */
    public function stopAllSessions(User $user): int
    {
        if (!$this->shouldRun()) {
            return 0;
        }

        return DB::table($this->table)
            ->where('user_id', $user->id)
            ->delete();
    }

    /**
     * Concurrent stream limit al
     */
    public function getConcurrentStreamLimit(User $user): int
    {
        // 1. User override (admin tarafından ayarlanabilir)
        if ($user->concurrent_streams_limit !== null && $user->concurrent_streams_limit > 0) {
            return $user->concurrent_streams_limit;
        }

        // 2. Subscription Plan
        $subscription = $user->subscriptions()
            ->whereIn('status', ['active', 'trial'])
            ->where(function($q) {
                $q->whereNull('current_period_end')
                  ->orWhere('current_period_end', '>', now());
            })
            ->with('plan')
            ->first();

        if ($subscription && $subscription->plan) {
            // Önce concurrent_streams_limit, yoksa device_limit'e fallback
            if ($subscription->plan->concurrent_streams_limit) {
                return (int) $subscription->plan->concurrent_streams_limit;
            }
            if ($subscription->plan->device_limit) {
                return (int) $subscription->plan->device_limit;
            }
        }

        // 3. Tenant setting fallback
        if (function_exists('setting') && setting('concurrent_streams_limit')) {
            return (int) setting('concurrent_streams_limit');
        }

        // 4. Default
        return 1;
    }

    /**
     * Aktif playback sayısı
     */
    public function getActivePlaybackCount(User $user): int
    {
        if (!$this->shouldRun()) {
            return 0;
        }

        return DB::table($this->table)
            ->where('user_id', $user->id)
            ->where('last_ping', '>', now()->subSeconds($this->pingTimeout))
            ->count();
    }

    /**
     * Kullanıcının aktif playback session'larını getir
     */
    public function getActiveSessions(User $user): array
    {
        if (!$this->shouldRun()) {
            return [];
        }

        return DB::table($this->table)
            ->where('user_id', $user->id)
            ->where('last_ping', '>', now()->subSeconds($this->pingTimeout))
            ->orderBy('last_ping', 'desc')
            ->get()
            ->map(function($session) {
                return [
                    'playback_id' => $session->playback_id,
                    'device_name' => $session->device_name,
                    'device_type' => $session->device_type,
                    'current_song_id' => $session->current_song_id,
                    'last_ping' => $session->last_ping,
                    'started_at' => $session->created_at,
                    'ip_address' => $session->ip_address,
                ];
            })
            ->toArray();
    }

    /**
     * Expired session'ları temizle (30 saniye ping almayan)
     */
    public function cleanupExpiredSessions(): int
    {
        return DB::table($this->table)
            ->where('last_ping', '<', now()->subSeconds($this->pingTimeout))
            ->delete();
    }

    /**
     * Device type belirle
     */
    protected function getDeviceType(Agent $agent): string
    {
        if ($agent->isMobile()) return 'mobile';
        if ($agent->isTablet()) return 'tablet';
        return 'desktop';
    }
}

3 API Controller - PlaybackController ~10 dakika

Modules/Muzibu/app/Http/Controllers/Api/PlaybackController.php YENİ
<?php

namespace Modules\Muzibu\App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Modules\Muzibu\App\Services\PlaybackSessionService;

class PlaybackController extends Controller
{
    public function __construct(
        private PlaybackSessionService $playbackService
    ) {}

    /**
     * POST /api/playback/start
     * Play butonuna basıldığında çağrılır
     */
    public function start(Request $request): JsonResponse
    {
        $request->validate([
            'playback_id' => 'required|uuid',
            'song_id' => 'nullable|integer',
        ]);

        $user = auth('web')->user() ?? auth('sanctum')->user();

        if (!$user) {
            return response()->json([
                'success' => false,
                'error' => 'unauthenticated',
            ], 401);
        }

        $result = $this->playbackService->registerPlaybackSession(
            $user,
            $request->playback_id,
            $request->song_id
        );

        if (!$result['success']) {
            return response()->json($result, 403);
        }

        return response()->json($result);
    }

    /**
     * POST /api/playback/ping
     * Her 10 saniyede bir heartbeat
     */
    public function ping(Request $request): JsonResponse
    {
        $request->validate([
            'playback_id' => 'required|uuid',
            'song_id' => 'nullable|integer',
        ]);

        $user = auth('web')->user() ?? auth('sanctum')->user();

        if (!$user) {
            return response()->json(['success' => false], 401);
        }

        $result = $this->playbackService->pingPlaybackSession(
            $user,
            $request->playback_id,
            $request->song_id
        );

        return response()->json($result);
    }

    /**
     * POST /api/playback/stop
     * Pause/Stop butonuna basıldığında
     */
    public function stop(Request $request): JsonResponse
    {
        $request->validate([
            'playback_id' => 'required|uuid',
        ]);

        $user = auth('web')->user() ?? auth('sanctum')->user();

        if (!$user) {
            return response()->json(['success' => false], 401);
        }

        $this->playbackService->stopPlaybackSession($user, $request->playback_id);

        return response()->json(['success' => true]);
    }

    /**
     * GET /api/playback/active
     * Aktif playback session'ları listele
     */
    public function active(): JsonResponse
    {
        $user = auth('web')->user() ?? auth('sanctum')->user();

        if (!$user) {
            return response()->json(['success' => false], 401);
        }

        $sessions = $this->playbackService->getActiveSessions($user);
        $limit = $this->playbackService->getConcurrentStreamLimit($user);

        return response()->json([
            'success' => true,
            'active_count' => count($sessions),
            'limit' => $limit,
            'sessions' => $sessions,
        ]);
    }

    /**
     * DELETE /api/playback/{playbackId}
     * Belirli bir session'ı zorla durdur
     */
    public function forceStop(Request $request, string $playbackId): JsonResponse
    {
        $user = auth('web')->user() ?? auth('sanctum')->user();

        if (!$user) {
            return response()->json(['success' => false], 401);
        }

        $this->playbackService->forceStopSession($user, $playbackId);

        return response()->json(['success' => true]);
    }
}
Modules/Muzibu/routes/api.php GÜNCELLE
// Playback Session Routes (Concurrent Limit)
Route::prefix('playback')->middleware('auth:sanctum')->group(function () {
    Route::post('/start', [PlaybackController::class, 'start']);
    Route::post('/ping', [PlaybackController::class, 'ping']);
    Route::post('/stop', [PlaybackController::class, 'stop']);
    Route::get('/active', [PlaybackController::class, 'active']);
    Route::delete('/{playbackId}', [PlaybackController::class, 'forceStop']);
});

4 Frontend JavaScript - PlaybackSession Module ~15 dakika

public/themes/muzibu/js/player/features/playback-session.js YENİ
/**
 * Muzibu Playback Session Manager
 * Concurrent playback limit için heartbeat sistemi
 *
 * Spotify Modeli: Sınırsız giriş, sınırlı playback
 */

const MuzibuPlaybackSession = {
    // State
    playbackId: null,
    heartbeatInterval: null,
    isRegistered: false,

    // Config
    HEARTBEAT_INTERVAL: 10000, // 10 saniye
    API_BASE: '/api/playback',

    /**
     * Playback ID oluştur veya mevcut olanı al
     * sessionStorage kullanılır (tab-specific)
     */
    getOrCreatePlaybackId() {
        if (this.playbackId) {
            return this.playbackId;
        }

        let id = sessionStorage.getItem('mzb_playback_id');
        if (!id) {
            id = crypto.randomUUID();
            sessionStorage.setItem('mzb_playback_id', id);
        }

        this.playbackId = id;
        return id;
    },

    /**
     * Playback session başlat
     * Play butonundan ÖNCE çağrılmalı
     *
     * @returns {Promise<{success: boolean, error?: string}>}
     */
    async startSession(songId = null) {
        const playbackId = this.getOrCreatePlaybackId();

        try {
            const response = await fetch(`${this.API_BASE}/start`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest',
                    'Referer': window.location.origin + '/',
                },
                credentials: 'same-origin',
                body: JSON.stringify({
                    playback_id: playbackId,
                    song_id: songId,
                }),
            });

            const data = await response.json();

            if (!response.ok || !data.success) {
                console.warn('🎵 Playback limit exceeded:', data);
                return {
                    success: false,
                    error: data.error || 'unknown',
                    message: data.message,
                    activeSessions: data.active_sessions || [],
                    limit: data.limit,
                };
            }

            // Başarılı - heartbeat başlat
            this.isRegistered = true;
            this.startHeartbeat(songId);

            console.log('🎵 Playback session started:', playbackId);

            return { success: true, playbackId };

        } catch (error) {
            console.error('🎵 Playback session start error:', error);
            // Network hatası - yine de çalmaya izin ver (graceful degradation)
            return { success: true, playbackId, offline: true };
        }
    },

    /**
     * Playback session durdur
     * Pause/Stop butonunda çağrılmalı
     */
    async stopSession() {
        if (!this.playbackId || !this.isRegistered) {
            return;
        }

        // Heartbeat'i hemen durdur
        this.stopHeartbeat();

        try {
            await fetch(`${this.API_BASE}/stop`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                credentials: 'same-origin',
                body: JSON.stringify({
                    playback_id: this.playbackId,
                }),
            });

            console.log('🎵 Playback session stopped');
        } catch (error) {
            console.error('🎵 Playback session stop error:', error);
        }

        this.isRegistered = false;
    },

    /**
     * Heartbeat başlat (10 saniyede bir ping)
     */
    startHeartbeat(currentSongId = null) {
        this.stopHeartbeat(); // Önceki varsa temizle

        this.heartbeatInterval = setInterval(async () => {
            if (!this.isRegistered) {
                this.stopHeartbeat();
                return;
            }

            try {
                const response = await fetch(`${this.API_BASE}/ping`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Accept': 'application/json',
                    },
                    credentials: 'same-origin',
                    body: JSON.stringify({
                        playback_id: this.playbackId,
                        song_id: this.currentSongId || currentSongId,
                    }),
                });

                const data = await response.json();

                if (!data.success) {
                    // Session artık geçerli değil
                    console.warn('🎵 Playback session expired:', data);
                    this.handleSessionExpired(data);
                }

            } catch (error) {
                console.error('🎵 Heartbeat error:', error);
                // Network hatası - devam et, belki geçicidir
            }

        }, this.HEARTBEAT_INTERVAL);

        console.log('🎵 Heartbeat started (10s interval)');
    },

    /**
     * Heartbeat durdur
     */
    stopHeartbeat() {
        if (this.heartbeatInterval) {
            clearInterval(this.heartbeatInterval);
            this.heartbeatInterval = null;
            console.log('🎵 Heartbeat stopped');
        }
    },

    /**
     * Session expired olduğunda
     */
    handleSessionExpired(data) {
        this.isRegistered = false;
        this.stopHeartbeat();

        // Player'ı durdur
        if (window.dispatchEvent) {
            window.dispatchEvent(new CustomEvent('playback:session-expired', {
                detail: data,
            }));
        }
    },

    /**
     * Şu anki şarkı ID'sini güncelle
     */
    updateCurrentSong(songId) {
        this.currentSongId = songId;
    },

    /**
     * Session durumunu kontrol et
     */
    isActive() {
        return this.isRegistered && this.heartbeatInterval !== null;
    },

    /**
     * Aktif session'ları getir
     */
    async getActiveSessions() {
        try {
            const response = await fetch(`${this.API_BASE}/active`, {
                credentials: 'same-origin',
            });
            return await response.json();
        } catch (error) {
            console.error('🎵 Get active sessions error:', error);
            return { sessions: [], limit: 1 };
        }
    },
};

// Global erişim için
window.MuzibuPlaybackSession = MuzibuPlaybackSession;

// Page unload'da session'ı durdur
window.addEventListener('beforeunload', () => {
    if (MuzibuPlaybackSession.isActive()) {
        // Sync XHR ile durdur (beforeunload'da async çalışmaz)
        navigator.sendBeacon(
            '/api/playback/stop',
            JSON.stringify({ playback_id: MuzibuPlaybackSession.playbackId })
        );
    }
});

5 Player-Core.js Entegrasyonu ~15 dakika

Kritik: player-core.js'te play fonksiyonlarına hook eklenecek. Her play() çağrısından ÖNCE session başlatılacak, her pause()'dan SONRA session durdurulacak.
public/themes/muzibu/js/player/core/player-core.js GÜNCELLE
// ============================================
// DEĞİŞİKLİK 1: loadAndPlaySong fonksiyonuna hook ekle
// ============================================

async loadAndPlaySong(url, streamType = null, previewDuration = null, autoplay = true) {
    // 🎵 PLAYBACK SESSION: Çalmadan önce session başlat
    if (autoplay && this.isLoggedIn) {
        const sessionResult = await MuzibuPlaybackSession.startSession(this.currentSong?.id);

        if (!sessionResult.success) {
            // Limit aşıldı - modal göster, çalma
            this.showPlaybackLimitModal(sessionResult);
            return;
        }
    }

    // ... mevcut kod devam eder ...
}

// ============================================
// DEĞİŞİKLİK 2: togglePlayPause fonksiyonuna hook ekle
// ============================================

async togglePlayPause() {
    if (this.isPlaying) {
        // PAUSE - Session'ı durdur
        await MuzibuPlaybackSession.stopSession();

        // ... mevcut pause kodu ...
    } else {
        // PLAY - Session başlat
        if (this.isLoggedIn) {
            const sessionResult = await MuzibuPlaybackSession.startSession(this.currentSong?.id);

            if (!sessionResult.success) {
                this.showPlaybackLimitModal(sessionResult);
                return;
            }
        }

        // ... mevcut play kodu ...
    }
}

// ============================================
// DEĞİŞİKLİK 3: stopCurrentPlayback fonksiyonuna hook ekle
// ============================================

stopCurrentPlayback() {
    // 🎵 PLAYBACK SESSION: Durdurulduğunda session'ı kapat
    MuzibuPlaybackSession.stopSession();

    // ... mevcut kod devam eder ...
}

// ============================================
// DEĞİŞİKLİK 4: Yeni fonksiyon - Limit Modal
// ============================================

showPlaybackLimitModal(result) {
    const sessions = result.activeSessions || [];
    const limit = result.limit || 1;

    // Modal HTML oluştur
    const modalHtml = `
        <div id="playback-limit-modal" class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm">
            <div class="bg-slate-900 border border-slate-700 rounded-2xl p-8 max-w-md mx-4 shadow-2xl">
                <div class="text-center">
                    <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-orange-500/20 flex items-center justify-center">
                        <svg class="w-8 h-8 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
                        </svg>
                    </div>
                    <h3 class="text-xl font-bold text-white mb-2">Eşzamanlı Çalma Limiti</h3>
                    <p class="text-slate-300 mb-4">
                        Bu abonelik zaten <strong>${sessions.length}</strong> yerde çalıyor.
                        <br>Limit: <strong>${limit}</strong> eşzamanlı stream.
                    </p>

                    ${sessions.length > 0 ? `
                    <div class="text-left mb-4">
                        <div class="text-sm text-slate-400 mb-2">Aktif Cihazlar:</div>
                        ${sessions.map(s => `
                            <div class="flex items-center justify-between bg-slate-800 rounded-lg p-3 mb-2">
                                <div>
                                    <div class="text-white text-sm">${s.device_name}</div>
                                    <div class="text-slate-500 text-xs">${s.ip_address}</div>
                                </div>
                                <button onclick="window.forceStopPlayback('${s.playback_id}')"
                                        class="px-3 py-1 bg-red-500/20 text-red-400 rounded hover:bg-red-500/30">
                                    Durdur
                                </button>
                            </div>
                        `).join('')}
                    </div>
                    ` : ''}

                    <button onclick="document.getElementById('playback-limit-modal').remove()"
                            class="w-full px-6 py-3 bg-slate-700 text-white font-semibold rounded-xl hover:bg-slate-600">
                        Tamam
                    </button>
                </div>
            </div>
        </div>
    `;

    // Eski modal varsa kaldır
    const existing = document.getElementById('playback-limit-modal');
    if (existing) existing.remove();

    // Modal'ı ekle
    document.body.insertAdjacentHTML('beforeend', modalHtml);
},

// ============================================
// DEĞİŞİKLİK 5: Global force stop fonksiyonu
// ============================================

// player-core.js init() içine ekle:
window.forceStopPlayback = async (playbackId) => {
    try {
        await fetch(`/api/playback/${playbackId}`, {
            method: 'DELETE',
            credentials: 'same-origin',
        });

        // Modal'ı kapat ve tekrar dene
        document.getElementById('playback-limit-modal')?.remove();
        this.showToast('Diğer cihaz durduruldu. Tekrar deneyin.', 'success');

    } catch (error) {
        console.error('Force stop error:', error);
    }
};

// ============================================
// DEĞİŞİKLİK 6: Session expired event listener
// ============================================

// player-core.js init() içine ekle:
window.addEventListener('playback:session-expired', (e) => {
    this.stopCurrentPlayback();
    this.isPlaying = false;
    this.showToast('Oturum başka bir yerden başlatıldı. Bu cihaz durduruldu.', 'warning');
});

6 Eski Sistem Kaldır (Device Limit) ~15 dakika

Önemli: Eski kodu silmiyoruz, devre dışı bırakıyoruz. 1 hafta sorunsuz çalışırsa tamamen kaldırılabilir.
app/Http/Controllers/Api/Auth/AuthController.php GÜNCELLE
// login() fonksiyonunda bu satırı KALDIR veya YORUMA AL:

// ❌ KALDIR:
// $deviceService->registerSession($user);
// $deviceService->handlePostLoginDeviceLimit($user);

// ✅ YENİ: Hiçbir device limit işlemi yok
// Playback limit player tarafında kontrol edilecek
public/themes/muzibu/js/player/features/session.js GÜNCELLE
// Session polling'i DEVRE DIŞI bırak

const MuzibuSession = {
    // ...

    startSessionPolling() {
        // ❌ ESKİ: Session polling başlatıyordu
        // ✅ YENİ: Artık çalışmıyor - Playback session kullanılıyor
        console.log('🔐 Session polling DEVRE DIŞI - Playback session kullanılıyor');
        return;

        // Eski kod yoruma alındı...
    },

    // Diğer fonksiyonlar da benzer şekilde return; ile bypass edilebilir
};

Admin Settings Değişikliği

Admin Panel → Settings → Auth bölümünde:
  • auth_device = false (devre dışı)
  • concurrent_streams_enabled = true (yeni setting)
  • concurrent_streams_limit = 1 (default limit)

1 Hafta Sonra Silinecekler

Dosyalar:
  • Modules/Muzibu/app/Services/DeviceService.php
  • Modules/Muzibu/app/Http/Controllers/Api/DeviceController.php
Database:
  • user_active_sessions tablosu (DROP)

7 Admin Panel Değişiklikleri ~10 dakika

Modules/Subscription/app/Models/SubscriptionPlan.php GÜNCELLE
protected $fillable = [
    // ... mevcut alanlar ...
    'device_limit',
    'concurrent_streams_limit', // ✅ YENİ EKLE
    // ...
];
Modules/Subscription/resources/views/admin/livewire/subscription-plan-manage-component.blade.php GÜNCELLE
<!-- device_limit field'ının yanına ekle -->

<!-- Eşzamanlı Stream Limiti -->
<div class="col-md-6">
    <label class="form-label">
        Eşzamanlı Stream Limiti
        <i class="fas fa-info-circle text-muted"
           data-bs-toggle="tooltip"
           title="Aynı anda kaç yerde müzik çalınabilir"></i>
    </label>
    <input type="number"
           class="form-control"
           wire:model="concurrent_streams_limit"
           min="1"
           max="10"
           placeholder="1">
    <small class="text-muted">
        Spotify modeli: Sınırsız giriş, sınırlı playback
    </small>
</div>
Modules/Subscription/app/Http/Livewire/Admin/SubscriptionPlanManageComponent.php GÜNCELLE
// Properties
public $concurrent_streams_limit = 1;

// Rules
protected $rules = [
    // ... mevcut kurallar ...
    'concurrent_streams_limit' => 'nullable|integer|min:1|max:10',
];

// mount() içinde
$this->concurrent_streams_limit = $plan->concurrent_streams_limit ?? 1;

// save() içinde
$plan->concurrent_streams_limit = $this->concurrent_streams_limit ?? 1;

8 Test Senaryoları ~20 dakika

1 Normal Çalma

  1. Giriş yap
  2. Herhangi bir şarkı çal
  3. Console'da "Playback session started" gör
  4. 10 saniye bekle, "Heartbeat" mesajları gör
  5. Pause yap, "Playback session stopped" gör
✓ Beklenen: Tüm loglar görünmeli

2 Limit Testi (2 Tab)

  1. Tab 1: Şarkı çal (çalıyor olmalı)
  2. Tab 2: Yeni sekmede aç
  3. Tab 2: Başka şarkı çalmayı dene
  4. Limit hatası modal'ı gör
  5. Tab 1'deki cihazı "Durdur" butonuyla durdur
  6. Tab 2'de tekrar dene (çalmalı)
✓ Beklenen: 2. tab'da limit hatası

3 Timeout Testi

  1. Tab 1: Şarkı çal
  2. Network'ü kes (DevTools → Offline)
  3. 30+ saniye bekle
  4. Network'ü aç
  5. Tab 2: Şarkı çalmayı dene
✓ Beklenen: Tab 2 çalabilmeli (Tab 1 timeout oldu)

4 Sınırsız Gözatma Testi

  1. Giriş yap
  2. 5 farklı sekmede siteyi aç
  3. Her sekmede farklı sayfaya git
  4. Playlist'lere bak, favorilere ekle
  5. Hiçbir yerde limit hatası alınmamalı
✓ Beklenen: Sınırsız gözatma (sadece play'de limit)

Debug Komutları

# Aktif session'ları gör
php artisan tinker
>>> DB::table('active_playback_sessions')->get()

# Tüm session'ları temizle (test için)
>>> DB::table('active_playback_sessions')->truncate()

# Belirli kullanıcının session'ları
>>> DB::table('active_playback_sessions')->where('user_id', 1)->get()

Uygulama Checklist

Yeni Dosyalar

Güncellenecek Dosyalar

Devre Dışı Bırakılacak

Test