from flask import Flask, Response, jsonify, redirect, render_template, request, send_file, session, url_for
import docker
import datetime as dt
import html
import ipaddress
import json
import os
import random
import re
import shutil
import socket
import subprocess
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
import xml.etree.ElementTree as ET
import zipfile
from collections import Counter
from werkzeug.utils import secure_filename
from zoneinfo import ZoneInfo

app = Flask(__name__)
app.secret_key = os.environ.get("PANEL_SECRET", "radyo-panel-secret")

STREAM_URL = os.environ.get("STREAM_URL", "")
PANEL_USER = os.environ.get("PANEL_USER", "admin")
PANEL_PASSWORD = os.environ.get("PANEL_PASSWORD", "admin123")
BASE_PATH = "/web-radyo"
HOST_BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
MUZIK_YOLU = os.path.join(BASE_PATH, "muzik")
JINGLE_YOLU = os.path.join(BASE_PATH, "jingle")
HABER_YOLU = os.path.join(BASE_PATH, "haber")
HABER_CIKTI_YOLU = os.path.join(HABER_YOLU, "bultenler")
STATE_YOLU = os.path.join(BASE_PATH, "state")
AYARLAR_YOLU = os.path.join(BASE_PATH, "ayarlar.json")
HABER_AYARLAR_YOLU = os.path.join(STATE_YOLU, "haber_bulteni.json")
HABER_TASLAK_YOLU = os.path.join(STATE_YOLU, "haber_bulteni_taslak.txt")
PLAYLIST_YOLU = os.path.join(STATE_YOLU, "current_playlist.m3u")
HISTORY_YOLU = os.path.join(STATE_YOLU, "last_played.json")
LISTENER_HISTORY_YOLU = os.path.join(STATE_YOLU, "listener_history.json")
LISTENER_GEO_CACHE_YOLU = os.path.join(STATE_YOLU, "listener_geo_cache.json")
BACKUP_YOLU = os.path.join(STATE_YOLU, "backups")
JINGLE_STATUS_YOLU = os.path.join(STATE_YOLU, "jingle_status.json")
JINGLE_AUDIT_YOLU = os.path.join(STATE_YOLU, "jingle_audit.json")
PLAYLIST_SKIP_AUDIT_YOLU = os.path.join(STATE_YOLU, "playlist_skip_audit.json")
SERVER_ACTION_LOG_YOLU = os.path.join(STATE_YOLU, "server_actions.json")
DURATION_CACHE_YOLU = os.path.join(STATE_YOLU, "duration_cache.json")
SHOW_HABER_CONFIG_YOLU = os.path.join(STATE_YOLU, "show_haber_recorder.json")
SHOW_HABER_TRIGGER_YOLU = os.path.join(STATE_YOLU, "show_haber_trigger.json")
LIQ_YOLU = "/etc/liquidsoap/main.liq"
ENV_YOLU = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env"))
SES_UZANTILARI = (".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac")
PLAY_HISTORY_LIMIT = 500
PANEL_HISTORY_LIMIT = 250
SCHEDULER_STARTED = False
MONITOR_STARTED = False
LAST_FIRED = set()
SONG_COUNTERS = {}
LAST_SONG_COUNTER_TRACK = {}
JINGLE_ROTATION_INDEX = {}
PLAYLIST_ROTATION_INDEX = {}
PENDING_AFTER_JINGLES = {}
PENDING_PLAYLIST_JINGLES = {}
LAST_TRACK_RID = None
LAST_MUSIC_FINISH_RID = None
SCHEDULER_PAUSED_UNTIL = None
LAST_SONG_JINGLE_TRACK = {}
BEFORE_END_FIRED = {}     # jingle_id -> rid: şarkı sonu geçişi tetiklendi mi
_LIQ_STATION_CMD = [None]  # cache: Liquidsoap remaining komutu (örn "Radyo_Pop")
INJECTED_PLAYLIST_JINGLES = set()
PLAYED_PLAYLIST_JINGLES = set()
PLAYLIST_ROUND_PLAYED = set()
LAST_PLAYLIST_ROUND_KEY = ""
ISTANBUL_TZ = ZoneInfo("Europe/Istanbul")
AUTO_HOUR_PARENT = "otomatik_saat_anonslari"
AUTO_HOUR_PARENT_NAME = "Otomatik Saat Anonsları"


def panel_base_url():
    scheme = request.headers.get("X-Forwarded-Proto", request.scheme).split(",", 1)[0].strip() or request.scheme
    host = request.host
    host_name = host.split(":", 1)[0].lower()
    if host_name not in {"localhost", "127.0.0.1", "::1"} and host_name.endswith("radyopop.site"):
        scheme = "https"
    return f"{scheme}://{host}".rstrip("/")


def varsayilan_ayarlar():
    return {
        "active_playlist": "all",
        "playlist_mode": "shuffle",
        "playlist_shuffle": {},
        "playlist_order": [],
        "playlist_group_names": {"normal": "Normal Anonslar", "saat": "Saat Anonsları"},
        "playlist_container_folders": [AUTO_HOUR_PARENT],
        "playlists": {},
        "artist_gap": 3,
        "track_repeat_hours": 6,
        "jingle_volume": 100,
        "jingle_transition": 0,
        "jingle_music_overlap": 1.5,
        "jingle_duck_volume": 35,
        "song_end_before": 2.0,
        "jingle_end_before": 2.0,
        "crossfade_enabled": True,
        "crossfade_duration": 6,
        "fade_in": 1,
        "fade_out": 4,
        "smart_crossfade": False,
        "normalize_audio": False,
        "audio_processing_enabled": False,
        "audio_preset": "pop_radio",
        "bass_gain": 3,
        "low_mid_gain": -1,
        "presence_gain": 2,
        "air_gain": 1.5,
        "multiband_strength": 2,
        "compressor_strength": 4,
        "output_gain": 0,
        "limiter_enabled": True,
        "station_name": "Radyo Pop",
        "station_genre": "Pop",
        "station_url": "",
        "station_description": "",
        "static_metadata": "",
        "empty_playlist_metadata": "Radyo",
        "auto_restart": True,
        "manual_stop": False,
        "recording_enabled": False,
        "silence_detection_enabled": True,
        "silence_seconds": 20,
        "silence_threshold": -45,
        "trim_silence_enabled": True,
        "trim_silence_seconds": 3,
        "trim_silence_threshold": -45,
        "ftp_password": "radyo123",
        "schedules": [],
        "ad_blocks": [],
        "banned_tracks": [],
        "favorite_tracks": [],
        "hourly_announcement_enabled": False,
        "hourly_announcement_folder": "",
        "auto_backup_enabled": True,
        "backup_keep_days": 14,
        "recording_keep_days": 14,
        "jingle_priority": ["ad_block", "time", "hourly", "songs"],
        "day_playlist": "",
        "night_playlist": "",
        "day_start": "07:00",
        "night_start": "22:00",
        "jingles": [],
        "live_relays": [],
        "active_live_relay_id": "",
        "dj_slots": [],
        "djs": [],
    }


def ayarlari_oku():
    if not os.path.exists(AYARLAR_YOLU):
        return varsayilan_ayarlar()
    try:
        with open(AYARLAR_YOLU, "r", encoding="utf-8") as f:
            data = json.load(f)
    except (json.JSONDecodeError, OSError):
        data = {}

    # Eski ayarlar.json formatini da kirilmadan yeni panele tasir.
    if "jingles" not in data:
        data["jingles"] = []
    if "live_relays" not in data:
        data["live_relays"] = []
    if "active_live_relay_id" not in data:
        data["active_live_relay_id"] = ""
    if "dj_slots" not in data:
        data["dj_slots"] = []
    if "djs" not in data:
        data["djs"] = []
    if "active_playlist" not in data:
        data["active_playlist"] = "all"
    if "playlist_mode" not in data:
        data["playlist_mode"] = "shuffle"
    if "playlist_shuffle" not in data:
        data["playlist_shuffle"] = {}
    if "playlist_order" not in data:
        data["playlist_order"] = []
    if "playlist_group_names" not in data:
        data["playlist_group_names"] = {"normal": "Normal Anonslar", "saat": "Saat Anonsları"}
    if "playlist_container_folders" not in data:
        data["playlist_container_folders"] = [AUTO_HOUR_PARENT]
    elif AUTO_HOUR_PARENT not in data.get("playlist_container_folders", []):
        data["playlist_container_folders"].append(AUTO_HOUR_PARENT)
    if "playlists" not in data:
        data["playlists"] = {}
    if "artist_gap" not in data:
        data["artist_gap"] = 3
    if "track_repeat_hours" not in data:
        data["track_repeat_hours"] = 6
    if "jingle_volume" not in data:
        data["jingle_volume"] = 100
    if "jingle_transition" not in data:
        data["jingle_transition"] = 0
    if "jingle_music_overlap" not in data:
        data["jingle_music_overlap"] = 1.5
    if "jingle_duck_volume" not in data:
        data["jingle_duck_volume"] = 35
    if "song_end_before" not in data:
        data["song_end_before"] = 2.0
    if "jingle_end_before" not in data:
        data["jingle_end_before"] = 2.0
    if "crossfade_enabled" not in data:
        data["crossfade_enabled"] = True
    if "crossfade_duration" not in data:
        data["crossfade_duration"] = 6
    if "fade_in" not in data:
        data["fade_in"] = 1
    if "fade_out" not in data:
        data["fade_out"] = 4
    if "smart_crossfade" not in data:
        data["smart_crossfade"] = False
    if "normalize_audio" not in data:
        data["normalize_audio"] = False
    if "audio_processing_enabled" not in data:
        data["audio_processing_enabled"] = False
    if "audio_preset" not in data:
        data["audio_preset"] = "pop_radio"
    if "bass_gain" not in data:
        data["bass_gain"] = 3
    if "low_mid_gain" not in data:
        data["low_mid_gain"] = -1
    if "presence_gain" not in data:
        data["presence_gain"] = 2
    if "air_gain" not in data:
        data["air_gain"] = 1.5
    if "multiband_strength" not in data:
        data["multiband_strength"] = 2
    if "compressor_strength" not in data:
        data["compressor_strength"] = 4
    if "output_gain" not in data:
        data["output_gain"] = 0
    if "limiter_enabled" not in data:
        data["limiter_enabled"] = True
    if "station_name" not in data:
        data["station_name"] = "Radyo Pop"
    if "station_genre" not in data:
        data["station_genre"] = "Pop"
    if "station_url" not in data:
        data["station_url"] = ""
    if "station_description" not in data:
        data["station_description"] = ""
    if "static_metadata" not in data:
        data["static_metadata"] = ""
    if "empty_playlist_metadata" not in data:
        data["empty_playlist_metadata"] = "Radyo"
    if "auto_restart" not in data:
        data["auto_restart"] = True
    if "manual_stop" not in data:
        data["manual_stop"] = False
    if "recording_enabled" not in data:
        data["recording_enabled"] = False
    if "silence_detection_enabled" not in data:
        data["silence_detection_enabled"] = True
    if "silence_seconds" not in data:
        data["silence_seconds"] = 20
    if "silence_threshold" not in data:
        data["silence_threshold"] = -45
    if "trim_silence_enabled" not in data:
        data["trim_silence_enabled"] = True
    if "trim_silence_seconds" not in data:
        data["trim_silence_seconds"] = 3
    if "trim_silence_threshold" not in data:
        data["trim_silence_threshold"] = data.get("silence_threshold", -45)
    if "ftp_password" not in data:
        data["ftp_password"] = "radyo123"
    if "schedules" not in data:
        data["schedules"] = []
    if "ad_blocks" not in data:
        data["ad_blocks"] = []
    if "banned_tracks" not in data:
        data["banned_tracks"] = []
    if "favorite_tracks" not in data:
        data["favorite_tracks"] = []
    if "hourly_announcement_enabled" not in data:
        data["hourly_announcement_enabled"] = False
    if "hourly_announcement_folder" not in data:
        data["hourly_announcement_folder"] = ""
    if "auto_backup_enabled" not in data:
        data["auto_backup_enabled"] = True
    if "backup_keep_days" not in data:
        data["backup_keep_days"] = 14
    if "recording_keep_days" not in data:
        data["recording_keep_days"] = 14
    if "jingle_priority" not in data:
        data["jingle_priority"] = ["ad_block", "time", "hourly", "songs"]
    if "day_playlist" not in data:
        data["day_playlist"] = ""
    if "night_playlist" not in data:
        data["night_playlist"] = ""
    if "day_start" not in data:
        data["day_start"] = "07:00"
    if "night_start" not in data:
        data["night_start"] = "22:00"
    for jingle in data.get("jingles", []):
        if "cut_policy" not in jingle:
            jingle["cut_policy"] = "after"
        if "active" not in jingle:
            jingle["active"] = True
        if "days" not in jingle:
            jingle["days"] = []
        if "start_time" not in jingle:
            jingle["start_time"] = ""
        if "end_time" not in jingle:
            jingle["end_time"] = ""
        if "paths" not in jingle:
            jingle["paths"] = [jingle.get("path", "")] if jingle.get("path") else []
        if "save_history" not in jingle:
            jingle["save_history"] = True
        if "custom_artist" not in jingle:
            jingle["custom_artist"] = ""
        if "custom_title" not in jingle:
            jingle["custom_title"] = ""
    return data


def ayarlari_yaz(ayarlar):
    os.makedirs(os.path.dirname(AYARLAR_YOLU), exist_ok=True)
    with open(AYARLAR_YOLU, "w", encoding="utf-8") as f:
        json.dump(ayarlar, f, ensure_ascii=False, indent=2)


def guvenli_docker_client():
    try:
        return docker.from_env()
    except docker.errors.DockerException:
        return None


def container_durumu():
    client = guvenli_docker_client()
    if client is None:
        return "DOCKER BAGLANTISI YOK"
    try:
        container = client.containers.get("radio-engine")
        return "YAYINDA" if container.status == "running" else "DURDURULDU"
    except docker.errors.NotFound:
        return "SUNUCU YOK"
    except docker.errors.DockerException:
        return "SUNUCU KAPALI"


def yayin_linki():
    if STREAM_URL:
        return STREAM_URL
    host = request.host.split(":", 1)[0]
    return f"http://{host}:8090/radyo"


def docker_komut(aksiyon):
    client = guvenli_docker_client()
    if client is None:
        return False
    try:
        container = client.containers.get("radio-engine")
        if aksiyon == "start":
            container.start()
        elif aksiyon == "stop":
            container.stop()
        elif aksiyon == "restart":
            container.restart()
        return True
    except docker.errors.DockerException as hata:
        print(f"Docker hatasi: {hata}")
        return False


def ses_dosyasi_mi(dosya):
    return dosya.lower().endswith(SES_UZANTILARI)


def rel_to_container_path(root, dosya):
    return os.path.join(root, dosya).replace("\\", "/")


def guvenli_kok(tip):
    if tip == "jingle":
        return JINGLE_YOLU
    if tip == "haber":
        return HABER_YOLU
    return MUZIK_YOLU


def guvenli_yol(tip, rel_path=""):
    kok = os.path.abspath(guvenli_kok(tip))
    hedef = os.path.abspath(os.path.join(kok, rel_path or ""))
    if hedef != kok and not hedef.startswith(kok + os.sep):
        raise ValueError("Gecersiz yol")
    return hedef


def klasorler(tip):
    kok = guvenli_kok(tip)
    sonuc = [""]
    if os.path.exists(kok):
        for root, dirs, _ in os.walk(kok):
            for klasor in dirs:
                tam = os.path.join(root, klasor)
                sonuc.append(os.path.relpath(tam, kok).replace("\\", "/"))
    return sorted(sonuc, key=str.lower)


def playlist_klasor_rel(playlist_key, ayarlar=None):
    if playlist_key == "all":
        return ""
    ayarlar = ayarlar or ayarlari_oku()
    cfg = ayarlar.get("playlists", {}).get(playlist_key, {})
    rel = str(cfg.get("folder_path") or playlist_key).strip().replace("\\", "/").strip("/")
    if not rel:
        rel = playlist_key
    # guvenli_yol asıl sınır kontrolünü yapar; burada sadece tekrar eden slash'ları toparlıyoruz.
    return re.sub(r"/+", "/", rel)


def playlist_klasor_yolu(playlist_key, ayarlar=None):
    if playlist_key == "all":
        return MUZIK_YOLU
    return guvenli_yol("muzik", playlist_klasor_rel(playlist_key, ayarlar))


def kutuphane_dosyalari(tip, rel_folder=""):
    klasor = guvenli_yol(tip, rel_folder)
    dosyalar = []
    if os.path.exists(klasor):
        for name in sorted(os.listdir(klasor), key=str.lower):
            tam = os.path.join(klasor, name)
            if os.path.isfile(tam) and ses_dosyasi_mi(name):
                rel = os.path.relpath(tam, guvenli_kok(tip)).replace("\\", "/")
                dosyalar.append({"name": name, "rel": rel, "size": os.path.getsize(tam)})
    return dosyalar


def disk_bilgisi():
    usage = shutil.disk_usage(BASE_PATH if os.path.exists(BASE_PATH) else "/")
    return {
        "total_gb": round(usage.total / (1024 ** 3), 1),
        "used_gb": round(usage.used / (1024 ** 3), 1),
        "free_gb": round(usage.free / (1024 ** 3), 1),
        "percent": round((usage.used / usage.total) * 100, 1),
    }


def env_yaz(ayarlar):
    ftp_password = str(ayarlar.get("ftp_password", "radyo123")).replace("\n", "").strip() or "radyo123"
    ftp_address = os.environ.get("FTP_ADDRESS", "127.0.0.1")
    with open(ENV_YOLU, "w", encoding="utf-8") as f:
        f.write(f"FTP_PASSWORD={ftp_password}\n")
        f.write(f"FTP_ADDRESS={ftp_address}\n")


def container_volumes(container_name, fallback=None):
    client = guvenli_docker_client()
    if client is None:
        return fallback or {}
    try:
        container = client.containers.get(container_name)
        volumes = {}
        for mount in container.attrs.get("Mounts", []):
            source = mount.get("Source")
            destination = mount.get("Destination")
            if source and destination:
                volumes[source] = {"bind": destination, "mode": mount.get("Mode") or "rw"}
        return volumes or (fallback or {})
    except docker.errors.DockerException:
        return fallback or {}


def container_network(container_name):
    client = guvenli_docker_client()
    if client is None:
        return None
    try:
        container = client.containers.get(container_name)
        networks = container.attrs.get("NetworkSettings", {}).get("Networks", {})
        return next(iter(networks.keys()), None)
    except docker.errors.DockerException:
        return None


def ftp_servislerini_yenile(ayarlar):
    client = guvenli_docker_client()
    if client is None:
        return False
    password = str(ayarlar.get("ftp_password", "radyo123")).replace("\n", "").strip() or "radyo123"
    env_yaz({"ftp_password": password})
    ftp_volumes = container_volumes("radio-ftp")
    sftp_volumes = container_volumes("radio-sftp")
    network = container_network("radio-ftp") or container_network("radio-sftp")
    try:
        for name in ("radio-ftp", "radio-sftp"):
            try:
                old = client.containers.get(name)
                old.stop(timeout=5)
                old.remove()
            except docker.errors.NotFound:
                pass
        run_kwargs = {
            "detach": True,
            "restart_policy": {"Name": "unless-stopped"},
        }
        if network:
            run_kwargs["network"] = network
        client.containers.run(
            "atmoz/sftp",
            f"radyo:{password}:1000",
            name="radio-sftp",
            ports={"22/tcp": 2222},
            volumes=sftp_volumes,
            **run_kwargs,
        )
        ftp_ports = {"21/tcp": 21}
        for port in range(21000, 21011):
            ftp_ports[f"{port}/tcp"] = port
        client.containers.run(
            "delfer/alpine-ftp-server",
            name="radio-ftp",
            ports=ftp_ports,
            volumes=ftp_volumes,
            environment={
                "USERS": f"radyo|{password}|/home/radyo",
                "ADDRESS": os.environ.get("FTP_ADDRESS", "127.0.0.1"),
                "MIN_PORT": "21000",
                "MAX_PORT": "21010",
            },
            **run_kwargs,
        )
        return True
    except docker.errors.DockerException as hata:
        print(f"FTP yenileme hatasi: {hata}")
        return False


def icecast_source_info():
    try:
        with urllib.request.urlopen("http://icecast-server:8000/status-json.xsl", timeout=2) as response:
            data = json.loads(response.read().decode("utf-8", errors="ignore"))
        source = data.get("icestats", {}).get("source", {})
        if isinstance(source, list):
            source = next((s for s in source if s.get("listenurl", "").endswith("/radyo") or s.get("server_name")), source[0] if source else {})
        return source if isinstance(source, dict) else {}
    except Exception:
        return {}


def listener_sayisi():
    source = icecast_source_info()
    return int(source.get("listeners", 0) or 0)


def bitrate_degeri(source):
    raw = source.get("bitrate") or source.get("audio_info", "")
    if isinstance(raw, (int, float)):
        return int(raw)
    match = re.search(r"bitrate=(\d+)", str(raw))
    if match:
        return int(match.group(1))
    match = re.search(r"(\d+)", str(raw))
    return int(match.group(1)) if match else 0


def yayin_kalite_bilgisi():
    source = icecast_source_info()
    bitrate = bitrate_degeri(source)
    listeners = int(source.get("listeners", 0) or 0)
    per_listener_kbs = round((bitrate / 8) * 1.08, 1) if bitrate else 0
    total_kbs = round(per_listener_kbs * listeners, 1) if per_listener_kbs else 0
    return {
        "bitrate": f"{bitrate} kbps" if bitrate else "-",
        "per_listener": f"{per_listener_kbs} KB/sn" if per_listener_kbs else "-",
        "estimated_traffic": f"{total_kbs} KB/sn" if per_listener_kbs else "-",
        "listeners": listeners,
    }


def listener_geo_cache_oku():
    if not os.path.exists(LISTENER_GEO_CACHE_YOLU):
        return {}
    try:
        with open(LISTENER_GEO_CACHE_YOLU, "r", encoding="utf-8") as f:
            data = json.load(f)
        return data if isinstance(data, dict) else {}
    except (json.JSONDecodeError, OSError):
        return {}


def listener_geo_cache_yaz(cache):
    os.makedirs(STATE_YOLU, exist_ok=True)
    items = list(cache.items())[-1000:]
    with open(LISTENER_GEO_CACHE_YOLU, "w", encoding="utf-8") as f:
        json.dump(dict(items), f, ensure_ascii=False, indent=2)


def ip_public_mi(ip):
    try:
        parsed = ipaddress.ip_address(str(ip).split(",", 1)[0].strip())
        return not (parsed.is_private or parsed.is_loopback or parsed.is_link_local or parsed.is_multicast)
    except ValueError:
        return False


def icecast_listener_clients():
    url = "http://icecast-server:8000/admin/listclients?mount=/radyo"
    request_obj = urllib.request.Request(url)
    request_obj.add_header("Authorization", "Basic YWRtaW46aGFja21l")
    try:
        with urllib.request.urlopen(request_obj, timeout=3) as response:
            raw = response.read()
        root = ET.fromstring(raw)
    except Exception:
        return []

    clients = []
    for listener in root.findall(".//listener"):
        ip = (listener.findtext("IP") or listener.findtext("ip") or "").strip()
        if not ip:
            continue
        clients.append({
            "ip": ip,
            "user_agent": (listener.findtext("UserAgent") or listener.findtext("useragent") or "-").strip(),
            "connected": (listener.findtext("Connected") or listener.findtext("connected") or "").strip(),
        })
    return clients


def ip_konum_bilgisi(ip, cache=None):
    cache = cache if cache is not None else listener_geo_cache_oku()
    public_ip = str(ip).split(",", 1)[0].strip()
    if not ip_public_mi(public_ip):
        return {
            "ip": public_ip,
            "country": "Yerel ag",
            "city": "Sunucu / yerel",
            "region": "",
            "lat": None,
            "lon": None,
            "source": "local",
        }
    if public_ip in cache:
        return cache[public_ip]
    info = {
        "ip": public_ip,
        "country": "Bilinmiyor",
        "city": "-",
        "region": "",
        "lat": None,
        "lon": None,
        "source": "unresolved",
    }
    try:
        fields = "status,country,city,regionName,lat,lon,query"
        with urllib.request.urlopen(f"http://ip-api.com/json/{public_ip}?fields={fields}", timeout=2) as response:
            data = json.loads(response.read().decode("utf-8", errors="ignore"))
        if data.get("status") == "success":
            info = {
                "ip": data.get("query", public_ip),
                "country": data.get("country") or "Bilinmiyor",
                "city": data.get("city") or "-",
                "region": data.get("regionName") or "",
                "lat": data.get("lat"),
                "lon": data.get("lon"),
                "source": "ip-api",
            }
    except Exception:
        pass
    cache[public_ip] = info
    return info


def dinleyici_haritasi():
    clients = icecast_listener_clients()
    cache = listener_geo_cache_oku()
    enriched = []
    changed = False
    for client in clients:
        before = len(cache)
        geo = ip_konum_bilgisi(client.get("ip", ""), cache)
        changed = changed or len(cache) != before
        row = dict(client)
        row.update(geo)
        enriched.append(row)
    if changed:
        listener_geo_cache_yaz(cache)

    country_counts = Counter(item.get("country") or "Bilinmiyor" for item in enriched)
    city_counts = Counter(
        f"{item.get('city') or '-'}, {item.get('country') or 'Bilinmiyor'}"
        for item in enriched
        if item.get("city") and item.get("city") != "-"
    )
    points = []
    for item in enriched:
        lat = item.get("lat")
        lon = item.get("lon")
        if lat is None or lon is None:
            continue
        try:
            lat_float = float(lat)
            lon_float = float(lon)
        except (TypeError, ValueError):
            continue
        points.append({
            "left": max(0, min(100, (lon_float + 180) / 360 * 100)),
            "top": max(0, min(100, (90 - lat_float) / 180 * 100)),
            "lat": lat_float,
            "lon": lon_float,
            "label": f"{item.get('city') or '-'}, {item.get('country') or 'Bilinmiyor'}",
            "ip": item.get("ip", ""),
        })
    return {
        "clients": enriched,
        "countries": country_counts.most_common(10),
        "cities": city_counts.most_common(10),
        "points": points,
        "total": len(enriched),
    }


def listener_gecmisi_oku():
    if not os.path.exists(LISTENER_HISTORY_YOLU):
        return []
    try:
        with open(LISTENER_HISTORY_YOLU, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return []


def listener_gecmisi_yaz(history):
    os.makedirs(STATE_YOLU, exist_ok=True)
    with open(LISTENER_HISTORY_YOLU, "w", encoding="utf-8") as f:
        json.dump(history[-288:], f, ensure_ascii=False, indent=2)


def listener_kaydet():
    history = listener_gecmisi_oku()
    now = dt.datetime.now(ISTANBUL_TZ)
    if history and history[-1].get("time", "")[:16] == now.strftime("%Y-%m-%d %H:%M"):
        return
    history.append({"time": now.strftime("%Y-%m-%d %H:%M"), "listeners": listener_sayisi()})
    listener_gecmisi_yaz(history)


def jingle_status_yaz(**kwargs):
    os.makedirs(STATE_YOLU, exist_ok=True)
    status = {}
    if os.path.exists(JINGLE_STATUS_YOLU):
        try:
            with open(JINGLE_STATUS_YOLU, "r", encoding="utf-8") as f:
                status = json.load(f)
        except (json.JSONDecodeError, OSError):
            status = {}
    status.update(kwargs)
    status["updated_at"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
    try:
        with open(JINGLE_STATUS_YOLU, "w", encoding="utf-8") as f:
            json.dump(status, f, ensure_ascii=False, indent=2)
    except OSError:
        pass
    jingle_audit_ekle(kwargs)


def jingle_status_oku():
    if not os.path.exists(JINGLE_STATUS_YOLU):
        return {}
    try:
        with open(JINGLE_STATUS_YOLU, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return {}


def jingle_audit_oku(limit=80):
    if not os.path.exists(JINGLE_AUDIT_YOLU):
        return []
    try:
        with open(JINGLE_AUDIT_YOLU, "r", encoding="utf-8") as f:
            return json.load(f)[-limit:]
    except (json.JSONDecodeError, OSError):
        return []


def jingle_audit_ekle(event):
    anlamli_alanlar = {
        "last_queue", "last_queue_name", "last_queue_success", "last_queue_reason",
        "last_time_match", "last_song_rule", "last_after_fire", "last_added",
    }
    if not any(key in event for key in anlamli_alanlar):
        return
    kayit = {
        "time": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
        "event": event.get("last_queue") or event.get("last_queue_reason") or event.get("last_added") or "Jingle",
        "name": event.get("last_queue_name") or event.get("last_time_match") or event.get("last_song_rule") or event.get("last_after_fire") or "",
        "success": event.get("last_queue_success", ""),
        "reason": event.get("last_queue_reason", ""),
        "track": event.get("last_song_rule_track") or event.get("last_after_track") or "",
    }
    history = jingle_audit_oku(200)
    if history and history[-1].get("event") == kayit["event"] and history[-1].get("name") == kayit["name"] and history[-1].get("time") == kayit["time"]:
        return
    history.append(kayit)
    try:
        with open(JINGLE_AUDIT_YOLU, "w", encoding="utf-8") as f:
            json.dump(history[-200:], f, ensure_ascii=False, indent=2)
    except OSError:
        pass


def sunucu_islem_kaydi(action, success=True, reason=""):
    os.makedirs(STATE_YOLU, exist_ok=True)
    kayitlar = []
    if os.path.exists(SERVER_ACTION_LOG_YOLU):
        try:
            with open(SERVER_ACTION_LOG_YOLU, "r", encoding="utf-8") as f:
                kayitlar = json.load(f)
        except (json.JSONDecodeError, OSError):
            kayitlar = []
    kayitlar.append({
        "time": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
        "action": action,
        "success": success,
        "reason": reason,
        "ip": request.headers.get("X-Forwarded-For", request.remote_addr or ""),
        "method": request.method,
        "path": request.path,
        "user_agent": request.headers.get("User-Agent", ""),
    })
    try:
        with open(SERVER_ACTION_LOG_YOLU, "w", encoding="utf-8") as f:
            json.dump(kayitlar[-200:], f, ensure_ascii=False, indent=2)
    except OSError:
        pass


def sunucu_islem_kaydi_oku(limit=80):
    if not os.path.exists(SERVER_ACTION_LOG_YOLU):
        return []
    try:
        with open(SERVER_ACTION_LOG_YOLU, "r", encoding="utf-8") as f:
            return json.load(f)[-limit:]
    except (json.JSONDecodeError, OSError):
        return []


def zaman_parse(text):
    if not text:
        return None
    for fmt in ("%d.%m.%Y %H:%M:%S", "%Y-%m-%d %H:%M", "%d.%m.%Y %H:%M"):
        try:
            return dt.datetime.strptime(text, fmt).replace(tzinfo=ISTANBUL_TZ)
        except (TypeError, ValueError):
            continue
    return None


def yakin_gecmis_muzik_dosyalari(saat=6):
    try:
        saat = max(0, float(saat))
    except (TypeError, ValueError):
        saat = 6
    if saat <= 0:
        return set()
    simdi = dt.datetime.now(ISTANBUL_TZ)
    sonuc = set()
    for item in gecmis_oku():
        if jingle_gecmis_kaydi_mi(item):
            continue
        zaman = zaman_parse(item.get("time", ""))
        if not zaman or (simdi - zaman).total_seconds() > saat * 3600:
            continue
        filename = item.get("filename")
        if filename:
            sonuc.add(filename)
    return sonuc


def tekrar_eden_sarkilari_arkaya_al(dosyalar, ayarlar):
    try:
        repeat_hours = max(0, float(ayarlar.get("track_repeat_hours", 6)))
    except (TypeError, ValueError):
        repeat_hours = 6
    recent = yakin_gecmis_muzik_dosyalari(repeat_hours)
    if not recent or len(dosyalar) < 4:
        return dosyalar
    taze = [path for path in dosyalar if path not in recent]
    tekrar = [path for path in dosyalar if path in recent]
    if not taze:
        return dosyalar
    return taze + tekrar


def jingle_ozeti():
    audit = jingle_audit_oku(200)
    basarili = sum(1 for item in audit if item.get("success") is True)
    basarisiz = sum(1 for item in audit if item.get("success") is False)
    son = audit[-1] if audit else {}
    return {
        "total": len(audit),
        "success": basarili,
        "failed": basarisiz,
        "last": son,
    }


def yayin_rapor_ozetleri():
    history = gecmis_oku()[:500]
    music_items = [item for item in history if item.get("source") == "music"]
    jingle_items = [item for item in history if item.get("source", "").startswith("jingle")]
    track_counter = Counter((item.get("title") or os.path.basename(item.get("filename", "")) or "-") for item in music_items)
    artist_counter = Counter((item.get("artist") or "-") for item in music_items)
    listener_history = listener_gecmisi_oku()
    peak = max(listener_history, key=lambda item: int(item.get("listeners", 0) or 0), default={})
    return {
        "track_total": len(music_items),
        "jingle_total": len(jingle_items),
        "top_tracks": track_counter.most_common(10),
        "top_artists": artist_counter.most_common(10),
        "listener_peak": peak,
    }


def yayin_kesinti_ozeti():
    actions = sunucu_islem_kaydi_oku(120)
    stops = [item for item in actions if item.get("action") == "stop"]
    blocked = [item for item in actions if item.get("action", "").endswith("-blocked")]
    last_action = actions[-1] if actions else {}
    return {
        "actions": list(reversed(actions[-30:])),
        "stops": len(stops),
        "blocked": len(blocked),
        "last_action": last_action,
        "last_stop": stops[-1] if stops else {},
    }


def kalite_taramasi(limit=80):
    sonuc = []
    for item in tum_muzik_dosyalari():
        path = item["path"]
        try:
            stat = os.stat(path)
        except OSError:
            continue
        sure = dosya_suresi_saniye(path)
        sorunlar = []
        if stat.st_size < 200 * 1024:
            sorunlar.append("çok küçük dosya")
        if sure and sure < 45:
            sorunlar.append("çok kısa")
        if " - " not in os.path.splitext(os.path.basename(path))[0]:
            sorunlar.append("artist - başlık formatı eksik")
        if sorunlar:
            sonuc.append({"name": item["name"], "path": path, "duration": sure_formatla(sure), "issues": ", ".join(sorunlar)})
        if len(sonuc) >= limit:
            break
    return sonuc


def yayin_akisi(ayarlar):
    sonuc = []
    for rule in ayarlar.get("schedules", []):
        sonuc.append({"time": rule.get("time", ""), "type": "Playlist", "name": rule.get("name") or rule.get("playlist", "")})
    for block in ayarlar.get("ad_blocks", []):
        hour = block.get("hour", "")
        minute = int(block.get("minute", 0))
        zaman = f"{int(hour):02d}:{minute:02d}" if str(hour) not in ("", "None") else f"Her saat {minute:02d}"
        sonuc.append({"time": zaman, "type": "Reklam bloğu", "name": block.get("name", "Reklam bloğu")})
    for jingle in ayarlar.get("jingles", []):
        if jingle.get("mode") != "time":
            continue
        hour = jingle.get("hour", "")
        minute = int(jingle.get("minute", 0))
        zaman = f"{int(hour):02d}:{minute:02d}" if str(hour) not in ("", "None") else f"Her saat {minute:02d}"
        sonuc.append({"time": zaman, "type": "Jingle", "name": jingle.get("name", "")})
    if ayarlar.get("hourly_announcement_enabled"):
        sonuc.append({"time": "Her saat 00", "type": "Saat anonsu", "name": ayarlar.get("hourly_announcement_folder", "") or "Klasör seçilmedi"})
    return sorted(sonuc, key=lambda item: item["time"])


def yaklasan_yayin_akisi(ayarlar, dakika=90):
    simdi = dt.datetime.now(ISTANBUL_TZ)
    bitis = simdi + dt.timedelta(minutes=dakika)
    sonuc = []
    gunler = {str(simdi.weekday()), ""}
    for rule in ayarlar.get("schedules", []):
        if not rule.get("active", True):
            continue
        days = [str(day) for day in (rule.get("days") or [])]
        if days and str(simdi.weekday()) not in days:
            continue
        try:
            saat, dakika_str = rule.get("time", "00:00").split(":")[:2]
            zaman = simdi.replace(hour=int(saat), minute=int(dakika_str), second=0, microsecond=0)
        except (ValueError, TypeError):
            continue
        if simdi <= zaman <= bitis:
            sonuc.append({"time": zaman.strftime("%H:%M"), "type": "Playlist", "name": rule.get("name") or rule.get("playlist", "")})
    for block in ayarlar.get("ad_blocks", []):
        if not block.get("active", True):
            continue
        minute = int(block.get("minute", 0))
        hours = range(24) if str(block.get("hour", "")) in ("", "None") else [int(block.get("hour"))]
        for hour in hours:
            zaman = simdi.replace(hour=hour, minute=minute, second=0, microsecond=0)
            if simdi <= zaman <= bitis:
                sonuc.append({"time": zaman.strftime("%H:%M"), "type": "Reklam", "name": block.get("name", "Reklam bloğu")})
    for jingle in ayarlar.get("jingles", []):
        if not jingle.get("active", True) or jingle.get("mode") != "time":
            continue
        if not jingle_zamaninda_mi(jingle, simdi):
            continue
        minute = int(jingle.get("minute", 0))
        hours = range(24) if str(jingle.get("hour", "")) in ("", "None") else [int(jingle.get("hour"))]
        for hour in hours:
            zaman = simdi.replace(hour=hour, minute=minute, second=0, microsecond=0)
            if simdi <= zaman <= bitis:
                sonuc.append({"time": zaman.strftime("%H:%M"), "type": "Jingle", "name": jingle.get("name", "")})
    if ayarlar.get("hourly_announcement_enabled"):
        for hour in range(simdi.hour, min(24, bitis.hour + 2)):
            zaman = simdi.replace(hour=hour, minute=0, second=0, microsecond=0)
            if simdi <= zaman <= bitis:
                sonuc.append({"time": zaman.strftime("%H:%M"), "type": "Saat anonsu", "name": ayarlar.get("hourly_announcement_folder", "") or "Klasör seçilmedi"})
    for item in yaklasan_playlist_kurallari(ayarlar, dakika):
        sonuc.append(item)
    return sorted(sonuc, key=lambda item: item["time"])[:30]


def yaklasan_playlist_kurallari(ayarlar, dakika=180):
    simdi = dt.datetime.now(ISTANBUL_TZ).replace(second=0, microsecond=0)
    bitis = simdi + dt.timedelta(minutes=dakika)
    sonuc = []
    cursor = simdi
    while cursor <= bitis:
        for key, playlist in ayarlar.get("playlists", {}).items():
            if not playlist.get("active", True):
                continue
            playback_type = playlist.get("playback_type", "general")
            if playback_type not in {"once_per_x_minutes", "once_per_hour"}:
                continue
            if playlist.get("source", "songs") != "songs" or not playlist_zaman_penceresinde_mi(playlist, cursor):
                continue
            try:
                interval_value = int(playlist.get("interval_value", 1))
            except (TypeError, ValueError):
                interval_value = 1
            if playback_type == "once_per_x_minutes":
                interval = max(1, interval_value)
                if cursor.minute % interval != 0:
                    continue
                rule = f"Her {interval} dakika"
            else:
                minute = interval_value
                schedule = playlist.get("schedule", {}) or {}
                if schedule.get("start_time"):
                    try:
                        minute = dt.time.fromisoformat(schedule["start_time"]).minute
                    except ValueError:
                        minute = interval_value
                minute = max(0, min(59, minute))
                if cursor.minute != minute:
                    continue
                rule = f"Saatin {minute}. dakikası"
            sonuc.append({
                "time": cursor.strftime("%H:%M"),
                "type": "Playlist",
                "name": playlist.get("name") or key,
                "key": key,
                "rule": rule,
            })
        cursor += dt.timedelta(minutes=1)
    return sorted(sonuc, key=lambda item: item["time"])


def siradaki_planli_playlist(ayarlar, dakika=180):
    items = yaklasan_playlist_kurallari(ayarlar, dakika)
    return items[0] if items else {}


def yayin_akisi_cakismalari(ayarlar, dakika=180):
    groups = {}
    for item in yaklasan_yayin_akisi(ayarlar, dakika):
        groups.setdefault(item.get("time", ""), []).append(item)
    conflicts = []
    for time, items in sorted(groups.items()):
        if not time or len(items) <= 1:
            continue
        hour_winner = next((item for item in items if item.get("rule", "").startswith("Saatin") or "Saat" in item.get("name", "")), None)
        entries = []
        for item in items:
            action = "Çalacak"
            if hour_winner and item is not hour_winner:
                action = "Atlanacak"
            elif hour_winner and item is hour_winner:
                action = "Öncelikli"
            entries.append({**item, "action": action})
        conflicts.append({"time": time, "entries": entries, "count": len(entries)})
    return conflicts


def playlist_skip_audit_oku():
    try:
        with open(PLAYLIST_SKIP_AUDIT_YOLU, "r", encoding="utf-8") as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return []


def playlist_skip_audit_yaz(items):
    os.makedirs(os.path.dirname(PLAYLIST_SKIP_AUDIT_YOLU), exist_ok=True)
    with open(PLAYLIST_SKIP_AUDIT_YOLU, "w", encoding="utf-8") as f:
        json.dump(items[:300], f, ensure_ascii=False, indent=2)


def playlist_skip_kaydi_ekle(playlist_key, playlist, simdi, reason):
    items = playlist_skip_audit_oku()
    kayit = {
        "time": simdi.strftime("%d.%m.%Y %H:%M:%S"),
        "minute": simdi.strftime("%H:%M"),
        "key": playlist_key,
        "name": playlist.get("name") or playlist_key,
        "reason": reason,
    }
    if items and items[0].get("key") == kayit["key"] and items[0].get("minute") == kayit["minute"]:
        return
    items.insert(0, kayit)
    playlist_skip_audit_yaz(items)


def playlist_dakika_tetiklenir_mi(playlist, simdi):
    playback_type = playlist.get("playback_type", "general")
    try:
        interval_value = int(playlist.get("interval_value", 1))
    except (TypeError, ValueError):
        interval_value = 1
    if playback_type == "once_per_x_minutes":
        interval = max(1, interval_value)
        if simdi.minute % interval == 0:
            return f"Her {interval} dakika"
        return ""
    if playback_type == "once_per_hour":
        schedule = playlist.get("schedule", {}) or {}
        schedule_times = [str(item) for item in (schedule.get("times") or []) if re.match(r"^\d{2}:\d{2}$", str(item))]
        if schedule_times:
            current_time = f"{simdi.hour:02d}:{simdi.minute:02d}"
            if current_time in schedule_times:
                return f"Planlı saat {current_time}"
            return ""
        minute = interval_value
        if schedule.get("start_time"):
            try:
                minute = dt.time.fromisoformat(schedule["start_time"]).minute
            except ValueError:
                minute = interval_value
        minute = max(0, min(59, minute))
        if simdi.minute == minute:
            return f"Saatin {minute}. dakikası"
    return ""


def kural_simulasyonu(ayarlar, saat=24, limit=140):
    simdi = dt.datetime.now(ISTANBUL_TZ).replace(second=0, microsecond=0)
    bitis = simdi + dt.timedelta(hours=saat)
    sonuc = []
    cursor = simdi
    while cursor <= bitis and len(sonuc) < limit:
        hour_exists = saat_anonsu_bu_dakikada_var_mi(ayarlar, cursor)
        for key, playlist in ayarlar.get("playlists", {}).items():
            if not playlist.get("active", True):
                continue
            if playlist.get("source", "songs") != "songs":
                continue
            if playlist.get("playback_type") not in {"once_per_x_minutes", "once_per_hour"}:
                continue
            if not playlist_zaman_penceresinde_mi(playlist, cursor):
                continue
            rule = playlist_dakika_tetiklenir_mi(playlist, cursor)
            if not rule:
                continue
            is_hour = saat_anonsu_playlisti_mi({"key": key, **playlist})
            skipped = hour_exists and not is_hour and playlist.get("playback_type") == "once_per_x_minutes"
            sonuc.append({
                "time": cursor.strftime("%H:%M"),
                "name": playlist.get("name") or key,
                "key": key,
                "rule": rule,
                "status": "Atlanacak" if skipped else ("Öncelikli" if is_hour else "Çalacak"),
                "reason": "Saat anonsu öncelikli" if skipped else "",
            })
            if len(sonuc) >= limit:
                break
        cursor += dt.timedelta(minutes=1)
    return sonuc


def playlist_istatistikleri(ayarlar):
    history = gecmis_oku()
    skips = playlist_skip_audit_oku()
    muzik_root = MUZIK_YOLU.replace("\\", "/")
    sonuc = []
    for liste in muzik_listeleri(ayarlar):
        if liste["key"] == "all":
            continue
        last_played = playlist_son_calma_bilgisi(liste, history)
        key = liste["key"]
        name = liste["name"]
        tracks = set(str(path).replace("\\", "/") for path in liste.get("manual_tracks", []))
        played_count = 0
        for item in history:
            filename = str(item.get("filename", "")).replace("\\", "/")
            title = str(item.get("title", "")).strip()
            if filename in tracks or (name and title == name) or filename.startswith(f"{muzik_root}/{key}/"):
                played_count += 1
        skipped = [item for item in skips if item.get("key") == key]
        sonuc.append({
            "key": key,
            "name": name,
            "group": playlist_gorunum_grubu(liste),
            "played_count": played_count,
            "last_played": last_played,
            "skipped_count": len(skipped),
            "last_skipped": skipped[0] if skipped else {},
            "active": liste.get("active", True),
        })
    return sonuc


def anons_gecmisi(limit=80):
    sonuc = []
    for item in gecmis_oku():
        filename = str(item.get("filename", "")).replace("\\", "/").lower()
        source = str(item.get("source", "")).lower()
        title = str(item.get("title", ""))
        is_hour = "/jingle/saat/" in filename or title.lower().startswith("saat ")
        is_announcement = "/jingle/" in filename or source in {"scheduled_next", "jingle_next", "jingle_now", "jingle_over"} or is_hour
        if not is_announcement:
            continue
        enriched = dict(item)
        enriched["kind"] = "Saat Anonsu" if is_hour else "Anons"
        sonuc.append(enriched)
        if len(sonuc) >= limit:
            break
    return sonuc


def playlist_saglik_kontrolu(ayarlar):
    issues = []
    playlists = ayarlar.get("playlists", {})
    for key, playlist in playlists.items():
        if not playlist.get("active", True):
            continue
        tracks = muzik_dosyalari(key)
        name = playlist.get("name") or key
        if not tracks:
            issues.append({
                "level": "danger",
                "title": "Boş çalma listesi",
                "detail": f"{name} aktif ama içinde çalacak dosya görünmüyor.",
            })
        missing = []
        for path in playlist.get("manual_tracks", []):
            if not os.path.exists(path):
                missing.append(path)
        if missing:
            issues.append({
                "level": "warning",
                "title": "Eksik bağlı dosya",
                "detail": f"{name} içinde {len(missing)} dosya bulunamadı.",
            })
        playback_type = playlist.get("playback_type", "general")
        if playback_type in {"once_per_x_minutes", "once_per_hour"} and playlist.get("display_group") not in {"normal", "saat"}:
            issues.append({
                "level": "info",
                "title": "Kategori önerisi",
                "detail": f"{name} zamanlı çalışıyor; Normal Anonslar veya Saat Anonsları kategorisine almak iyi olur.",
            })
    for rule in ayarlar.get("schedules", []):
        playlist_key = rule.get("playlist", "")
        if rule.get("active", True) and playlist_key and playlist_key not in playlists and not os.path.isdir(os.path.join(MUZIK_YOLU, playlist_key)):
            issues.append({
                "level": "warning",
                "title": "Takvimde eksik liste",
                "detail": f"{rule.get('name') or playlist_key} kuralı artık bulunmayan bir listeye bağlı.",
            })
    return issues[:12]


def playlist_temizlik_onerileri(ayarlar):
    sonuc = []
    for key, playlist in ayarlar.get("playlists", {}).items():
        name = playlist.get("name") or key
        tracks = muzik_dosyalari(key)
        missing = [path for path in playlist.get("manual_tracks", []) if not os.path.exists(path)]
        if playlist.get("active", True) and not tracks:
            sonuc.append({"key": key, "name": name, "type": "Boş aktif liste", "action": "Pasifleştirilebilir"})
        if missing:
            sonuc.append({"key": key, "name": name, "type": "Eksik dosya bağı", "action": f"{len(missing)} bozuk bağ temizlenebilir"})
    return sonuc[:40]


def saat_anons_dosya_yolu(hour):
    saat_adlari = [f"{hour}.mp3", f"{hour:02d}.mp3"]
    preferred_dirs = [
        os.path.join(JINGLE_YOLU, "jingle-saat-Radyo Pop Saat Jingleleri"),
        os.path.join(JINGLE_YOLU, "saat"),
    ]
    candidates = [
        os.path.join(folder, name)
        for folder in preferred_dirs
        for name in saat_adlari
    ]
    for path in candidates:
        if os.path.exists(path):
            return path
    if os.path.exists(JINGLE_YOLU):
        for root, _, files in os.walk(JINGLE_YOLU):
            root_name = os.path.basename(root).lower()
            if "saat" not in root_name:
                continue
            for file_name in sorted(files, key=str.lower):
                temiz = os.path.splitext(file_name)[0].lower()
                if ses_dosyasi_mi(file_name) and temiz in {str(hour), f"{hour:02d}"}:
                    return os.path.join(root, file_name)
    return candidates[0]


def saat_anonsu_dosya_baglarini_duzelt(ayarlar):
    degisti = False
    for key, playlist in ayarlar.get("playlists", {}).items():
        if not saat_anonsu_playlisti_mi({"key": key, **playlist}):
            continue
        try:
            hour = int(str(key).rsplit("_", 1)[-1])
        except ValueError:
            schedule = playlist.get("schedule") or {}
            try:
                hour = dt.time.fromisoformat(schedule.get("start_time", "00:00")).hour
            except ValueError:
                continue
        path = saat_anons_dosya_yolu(hour)
        if playlist.get("manual_tracks") != [path]:
            playlist["manual_tracks"] = [path]
            degisti = True
    return degisti


def yayin_sagligi():
    now = aktif_metadata()
    jstatus = jingle_status_oku()
    actions = yayin_kesinti_ozeti()
    last_action = actions.get("last_action") or {}
    quality = yayin_kalite_bilgisi()
    return {
        "container": container_durumu(),
        "listeners": quality.get("listeners", 0),
        "bitrate": quality.get("bitrate", "-"),
        "per_listener": quality.get("per_listener", "-"),
        "estimated_traffic": quality.get("estimated_traffic", "-"),
        "source": now.get("source") or "-",
        "remaining": now.get("remaining") or "-",
        "scheduler": "Çalışıyor" if jstatus.get("scheduler_alive") else "Bekleniyor",
        "last_jingle": jstatus.get("last_queue_name") or "-",
        "last_jingle_result": "Başarılı" if jstatus.get("last_queue_success") else (jstatus.get("last_queue_reason") or "-"),
        "last_action": last_action.get("action", "-"),
        "last_action_time": last_action.get("time", "-"),
        "last_action_ip": last_action.get("ip", "-"),
        "stop_count": actions.get("stops", 0),
        "blocked_count": actions.get("blocked", 0),
    }


def yedek_listesi():
    os.makedirs(BACKUP_YOLU, exist_ok=True)
    sonuc = []
    for name in sorted(os.listdir(BACKUP_YOLU), reverse=True):
        if not name.endswith(".zip"):
            continue
        path = os.path.join(BACKUP_YOLU, name)
        sonuc.append({
            "name": name,
            "size_mb": round(os.path.getsize(path) / (1024 * 1024), 2),
            "created": dt.datetime.fromtimestamp(os.path.getmtime(path), ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M"),
        })
    return sonuc


def kayit_kokleri():
    return [
        ("recordings", os.path.join(BASE_PATH, "recordings")),
        ("media/recordings", os.path.join(BASE_PATH, "media", "recordings")),
    ]


def kayit_listesi(limit=100):
    sonuc = []
    for etiket, kayit_kok in kayit_kokleri():
        if not os.path.exists(kayit_kok):
            continue
        for root, _, files in os.walk(kayit_kok):
            for name in files:
                if not name.lower().endswith((".mp3", ".wav", ".ogg")):
                    continue
                path = os.path.join(root, name)
                try:
                    stat = os.stat(path)
                except OSError:
                    continue
                rel = os.path.relpath(path, kayit_kok).replace("\\", "/")
                download_path = rel if etiket == "recordings" else f"{etiket}/{rel}"
                sonuc.append({
                    "name": download_path,
                    "download_path": download_path,
                    "size_mb": round(stat.st_size / (1024 * 1024), 2),
                    "created": dt.datetime.fromtimestamp(stat.st_mtime, ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M"),
                    "mtime": stat.st_mtime,
                })
    sonuc.sort(key=lambda item: item["mtime"], reverse=True)
    return sonuc[:limit]

def yedek_olustur():
    os.makedirs(BACKUP_YOLU, exist_ok=True)
    name = f"radyo-yedek-{dt.datetime.now(ISTANBUL_TZ):%Y%m%d-%H%M%S}.zip"
    hedef = os.path.join(BACKUP_YOLU, name)
    with zipfile.ZipFile(hedef, "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
        for path in (AYARLAR_YOLU, PLAYLIST_YOLU, HISTORY_YOLU, LISTENER_HISTORY_YOLU):
            if os.path.exists(path):
                zip_file.write(path, arcname=os.path.relpath(path, BASE_PATH))
    return name


def otomatik_yedek_kontrolu(ayarlar=None):
    ayarlar = ayarlar or ayarlari_oku()
    if not ayarlar.get("auto_backup_enabled"):
        return {"enabled": False, "created": False, "last": ayarlar.get("last_auto_backup", "")}
    today = dt.datetime.now(ISTANBUL_TZ).strftime("%Y-%m-%d")
    if ayarlar.get("last_auto_backup") == today:
        return {"enabled": True, "created": False, "last": today}
    name = yedek_olustur()
    ayarlar["last_auto_backup"] = today
    ayarlari_yaz(ayarlar)
    return {"enabled": True, "created": True, "last": today, "name": name}


def medya_onizleme_url(path):
    normalized = str(path or "").replace("\\", "/")
    roots = [
        ("jingle", JINGLE_YOLU.replace("\\", "/")),
        ("haber", HABER_YOLU.replace("\\", "/")),
        ("muzik", MUZIK_YOLU.replace("\\", "/")),
    ]
    for tip, root in roots:
        root = root.rstrip("/")
        if normalized.startswith(root + "/"):
            rel = normalized[len(root) + 1:]
            return f"/media/{tip}/{urllib.parse.quote(rel)}"
    return ""


def yedek_sil_dosyasi(name):
    safe_name = secure_filename(name)
    path = os.path.abspath(os.path.join(BACKUP_YOLU, safe_name))
    backup_root = os.path.abspath(BACKUP_YOLU)
    if path.startswith(backup_root + os.sep) and os.path.isfile(path):
        os.remove(path)
        return True
    return False


def eski_dosyalari_temizle(folder, keep_days, suffixes=None):
    try:
        keep_days = int(keep_days)
    except (TypeError, ValueError):
        keep_days = 14
    if keep_days <= 0 or not os.path.exists(folder):
        return 0
    cutoff = time.time() - (keep_days * 86400)
    silinen = 0
    for root, _, files in os.walk(folder):
        for file_name in files:
            if suffixes and not file_name.lower().endswith(suffixes):
                continue
            path = os.path.join(root, file_name)
            try:
                if os.path.getmtime(path) < cutoff:
                    os.remove(path)
                    silinen += 1
            except OSError:
                pass
    return silinen


def otomatik_yedek_kontrol(ayarlar):
    if not ayarlar.get("auto_backup_enabled", True):
        return
    bugun = dt.datetime.now(ISTANBUL_TZ).strftime("%Y-%m-%d")
    if ayarlar.get("last_auto_backup") == bugun:
        return
    yedek_olustur()
    ayarlar["last_auto_backup"] = bugun
    ayarlari_yaz(ayarlar)
    eski_dosyalari_temizle(BACKUP_YOLU, ayarlar.get("backup_keep_days", 14), (".zip",))
    eski_dosyalari_temizle(os.path.join(BASE_PATH, "recordings"), ayarlar.get("recording_keep_days", 14), (".mp3", ".wav", ".ogg"))


def duration_cache_oku():
    if not os.path.exists(DURATION_CACHE_YOLU):
        return {}
    try:
        with open(DURATION_CACHE_YOLU, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return {}


def duration_cache_yaz(cache):
    os.makedirs(STATE_YOLU, exist_ok=True)
    try:
        with open(DURATION_CACHE_YOLU, "w", encoding="utf-8") as f:
            json.dump(cache, f, ensure_ascii=False)
    except OSError:
        pass


def mp3_suresi_saniye(path):
    bitrates = {
        (3, 1): [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320],
        (2, 1): [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
        (0, 1): [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320],
        (3, 2): [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
        (2, 2): [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
        (0, 2): [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
        (3, 3): [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320],
        (2, 3): [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
        (0, 3): [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
    }
    sample_rates = {
        3: [44100, 48000, 32000],
        2: [22050, 24000, 16000],
        0: [11025, 12000, 8000],
    }
    size = os.path.getsize(path)
    total = 0.0
    with open(path, "rb") as f:
        data = f.read()
        offset = 0
        while offset + 4 <= len(data):
            if data[offset] == 0xFF and (data[offset + 1] & 0xE0) == 0xE0:
                header = int.from_bytes(data[offset:offset + 4], "big")
                version_id = (header >> 19) & 0x3
                layer_id = (header >> 17) & 0x3
                bitrate_id = (header >> 12) & 0xF
                sample_id = (header >> 10) & 0x3
                padding = (header >> 9) & 0x1
                if version_id == 1 or layer_id == 0 or bitrate_id in (0, 15) or sample_id == 3:
                    offset += 1
                    continue
                version = 3 if version_id == 3 else (2 if version_id == 2 else 0)
                layer = 4 - layer_id
                bitrate = bitrates.get((version, layer), [0] * 15)[bitrate_id] * 1000
                sample_rate = sample_rates[version][sample_id]
                if layer == 1:
                    frame_length = int(((12 * bitrate / sample_rate) + padding) * 4)
                    samples = 384
                else:
                    samples = 1152 if version == 3 or layer == 2 else 576
                    frame_length = int((144 * bitrate / sample_rate) + padding) if version == 3 else int((72 * bitrate / sample_rate) + padding)
                if frame_length <= 0:
                    offset += 1
                    continue
                total += samples / sample_rate
                offset += frame_length
            else:
                offset += 1
    if total <= 0:
        return int(size / 16000)
    return int(total)


def dosya_suresi_saniye(path):
    try:
        try:
            result = subprocess.run(
                ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path],
                capture_output=True,
                text=True,
                timeout=8,
            )
            if result.returncode == 0 and result.stdout.strip():
                return int(float(result.stdout.strip()))
        except (OSError, subprocess.SubprocessError, ValueError):
            pass
        if path.lower().endswith(".wav"):
            import wave
            with wave.open(path, "rb") as wav:
                return int(wav.getnframes() / float(wav.getframerate()))
        if path.lower().endswith(".mp3"):
            return mp3_suresi_saniye(path)
    except Exception:
        return 0
    return 0


def playlist_suresi_saniye(playlist_key):
    cache = duration_cache_oku()
    toplam = 0
    degisti = False
    for container_path in muzik_dosyalari(playlist_key):
        local_path = container_path
        try:
            stat = os.stat(local_path)
        except OSError:
            continue
        key = container_path
        marker = f"{int(stat.st_mtime)}:{stat.st_size}"
        item = cache.get(key, {})
        if item.get("marker") != marker:
            item = {"marker": marker, "duration": dosya_suresi_saniye(local_path)}
            cache[key] = item
            degisti = True
        toplam += int(item.get("duration", 0))
    if degisti:
        duration_cache_yaz(cache)
    return toplam


def sure_formatla(seconds):
    seconds = int(seconds or 0)
    hours, rem = divmod(seconds, 3600)
    minutes, secs = divmod(rem, 60)
    if hours:
        return f"{hours:02d}:{minutes:02d}:{secs:02d}"
    return f"{minutes:02d}:{secs:02d}"


def muzik_dosyalari(playlist_key="all", exclude_playlists=None):
    exclude_playlists = set(exclude_playlists or [])
    ayarlar = ayarlari_oku()
    if playlist_key == "all":
        kok = MUZIK_YOLU
    else:
        kok = playlist_klasor_yolu(playlist_key, ayarlar)

    banned = set(ayarlar.get("banned_tracks", []))
    container_folders = set(ayarlar.get("playlist_container_folders", [AUTO_HOUR_PARENT]))
    dosyalar = []
    if os.path.exists(kok):
        for root, _, files in os.walk(kok):
            if playlist_key == "all":
                rel_root = os.path.relpath(root, MUZIK_YOLU)
                top_folder = "" if rel_root == "." else rel_root.split(os.sep, 1)[0]
                if top_folder in exclude_playlists or top_folder in container_folders:
                    continue
            for file_name in sorted(files):
                if ses_dosyasi_mi(file_name):
                    path = rel_to_container_path(root, file_name)
                    if path not in banned:
                        dosyalar.append(path)
    if playlist_key != "all":
        ayarlar_cfg = ayarlar.get("playlists", {}).get(playlist_key, {})
        for path in ayarlar_cfg.get("manual_tracks", []):
            if path and path not in banned and os.path.exists(path) and path not in dosyalar:
                dosyalar.append(path)
    return dosyalar


def tum_muzik_dosyalari(limit=None):
    dosyalar = []
    if os.path.exists(MUZIK_YOLU):
        for root, _, files in os.walk(MUZIK_YOLU):
            for file_name in sorted(files, key=str.lower):
                if ses_dosyasi_mi(file_name):
                    path = rel_to_container_path(root, file_name)
                    dosyalar.append({"path": path, "name": os.path.relpath(path, MUZIK_YOLU).replace("\\", "/")})
                    if limit and len(dosyalar) >= limit:
                        return dosyalar
    return dosyalar


def mevcut_medya_dosyalari(limit=2500):
    dosyalar = []
    for item in tum_muzik_dosyalari(limit=None):
        dosyalar.append({"path": item["path"], "name": f"Müzik / {item['name']}"})
        if limit and len(dosyalar) >= limit:
            return dosyalar
    for item in jingle_dosyalari():
        dosyalar.append({"path": item["path"], "name": f"Jingle / {item['name']}"})
        if limit and len(dosyalar) >= limit:
            return dosyalar
    return dosyalar


def sanatci_adi(path):
    ad = os.path.basename(path)
    kok = os.path.splitext(ad)[0]
    if " - " in kok:
        return kok.split(" - ", 1)[0].strip().lower()
    return kok.strip().lower()


def temiz_dosya_basligi(path):
    name = os.path.splitext(os.path.basename(path))[0]
    name = re.sub(r"\s+", " ", name).strip()
    return name or os.path.basename(path)


def dosya_adindan_meta(path):
    title = temiz_dosya_basligi(path)
    artist = ""
    if " - " in title:
        artist, title = [part.strip() for part in title.split(" - ", 1)]
    elif title.startswith("(") and ")" in title:
        maybe_artist, rest = title[1:].split(")", 1)
        if maybe_artist.strip() and rest.strip():
            artist = maybe_artist.strip()
            title = rest.strip(" -_")
    return artist, title


def sanatci_tekrarini_azalt(dosyalar, artist_gap):
    if artist_gap <= 0 or len(dosyalar) < 3:
        return dosyalar
    kalan = list(dosyalar)
    sonuc = []
    son_sanatcilar = []
    while kalan:
        secim_index = 0
        for index, dosya in enumerate(kalan):
            if sanatci_adi(dosya) not in son_sanatcilar:
                secim_index = index
                break
        secilen = kalan.pop(secim_index)
        sonuc.append(secilen)
        son_sanatcilar.append(sanatci_adi(secilen))
        son_sanatcilar = son_sanatcilar[-artist_gap:]
    return sonuc


def playlist_karisik_mi(ayarlar, playlist_key):
    shuffle_map = ayarlar.get("playlist_shuffle", {})
    if playlist_key in shuffle_map:
        return bool(shuffle_map.get(playlist_key))
    playlist_cfg = ayarlar.get("playlists", {}).get(playlist_key, {})
    order = playlist_cfg.get("playback_order", "")
    if order in {"sequential", "ordered"}:
        return False
    if order in {"shuffle", "random"}:
        return True
    return ayarlar.get("playlist_mode") == "shuffle"


def playlist_sirala(dosyalar, ayarlar, playlist_key="all"):
    sirali = list(dosyalar)
    if playlist_karisik_mi(ayarlar, playlist_key):
        random.shuffle(sirali)
    sirali = tekrar_eden_sarkilari_arkaya_al(sirali, ayarlar)
    favorites = [path for path in ayarlar.get("favorite_tracks", []) if path in sirali]
    if favorites:
        for index, path in enumerate(favorites[: max(1, len(sirali) // 8)]):
            if path in sirali:
                sirali.remove(path)
                sirali.insert(min(index * 4, len(sirali)), path)
    try:
        artist_gap = max(0, int(ayarlar.get("artist_gap", 3)))
    except (TypeError, ValueError):
        artist_gap = 3
    return sanatci_tekrarini_azalt(sirali, artist_gap)


def sarki_arasi_jingle_kurallari(ayarlar):
    kurallar = []
    for jingle in ayarlar.get("jingles", []):
        if not jingle.get("active", True):
            continue
        if jingle.get("mode") != "songs" or jingle.get("cut_policy") != "after":
            continue
        paths = [path for path in jingle.get("paths", []) if path]
        if not paths and jingle.get("path"):
            paths = [jingle.get("path")]
        if not paths:
            continue
        try:
            every = max(1, int(jingle.get("every_songs", 1)))
        except (TypeError, ValueError):
            every = 1
        kurallar.append({"id": jingle.get("id", ""), "name": jingle.get("name", ""), "every": every, "paths": paths})
    return kurallar


def araya_girecek_playlistler(ayarlar, aktif_playlist):
    sonuc = []
    for key, playlist in ayarlar.get("playlists", {}).items():
        if key == aktif_playlist or not playlist.get("active", True):
            continue
        if playlist.get("source", "songs") != "songs":
            continue
        if playlist.get("playback_type") != "once_per_x_songs":
            continue
        if not playlist_zaman_penceresinde_mi(playlist, dt.datetime.now(ISTANBUL_TZ)):
            continue
        try:
            every = max(1, int(playlist.get("interval_value", 1)))
        except (TypeError, ValueError):
            every = 1
        dosyalar = playlist_sirala(muzik_dosyalari(key), ayarlar, key)
        if dosyalar:
            sonuc.append({
                "key": key,
                "name": playlist.get("name", key),
                "every": every,
                "tracks": dosyalar,
                "index": 0,
            })
    return sonuc


def araya_giren_playlist_anahtarlari(ayarlar, aktif_playlist):
    return [item["key"] for item in araya_girecek_playlistler(ayarlar, aktif_playlist)]


def yayin_akisi_dosyalari(muzikler, ayarlar, aktif_playlist="all"):
    kurallar = araya_girecek_playlistler(ayarlar, aktif_playlist)
    if not kurallar:
        return muzikler

    sonuc = []
    for index, muzik in enumerate(muzikler, start=1):
        sonuc.append(muzik)
        for kural in kurallar:
            if index % kural["every"] != 0:
                continue
            tracks = kural["tracks"]
            secilen = tracks[kural["index"] % len(tracks)]
            kural["index"] += 1
            sonuc.append(secilen)
    return sonuc


def gecici_playlist_eklerini_uygula(dosyalar, ayarlar):
    ekler = [ek for ek in ayarlar.get("scheduled_playlist_inserts", []) if ek.get("active", True)]
    if not ekler:
        return dosyalar
    sonuc = list(dosyalar)
    muzik_koku = MUZIK_YOLU.replace("\\", "/")
    for ek in ekler:
        path = (ek.get("path") or "").replace("\\", "/")
        after_track = (ek.get("after_track") or "").replace("\\", "/")
        if not path or not os.path.exists(path):
            continue
        if path in sonuc:
            continue
        try:
            index = sonuc.index(after_track)
        except ValueError:
            index = 0
        insert_at = index + 1
        while insert_at < len(sonuc) and not sonuc[insert_at].replace("\\", "/").startswith(muzik_koku):
            insert_at += 1
        sonuc.insert(insert_at, path)
    return sonuc


def dj_gecis_modu_aktif_mi(ayarlar):
    return bool(ayarlar.get("crossfade_enabled")) and bool(ayarlar.get("smart_crossfade"))


def muzik_sayilari():
    sayilar = {"all": 0}
    ayarlar = ayarlari_oku()
    container_folders = set(ayarlar.get("playlist_container_folders", [AUTO_HOUR_PARENT]))
    if not os.path.exists(MUZIK_YOLU):
        return sayilar
    for root, _, files in os.walk(MUZIK_YOLU):
        rel_root = os.path.relpath(root, MUZIK_YOLU)
        playlist_key = "" if rel_root == "." else rel_root.split(os.sep, 1)[0]
        for file_name in files:
            if not ses_dosyasi_mi(file_name):
                continue
            sayilar["all"] += 1
            if playlist_key and playlist_key not in container_folders:
                sayilar[playlist_key] = sayilar.get(playlist_key, 0) + 1
    for key in ayarlar.get("playlists", {}):
        if key != "all":
            sayilar[key] = len(muzik_dosyalari(key))
    return sayilar


def muzik_listeleri(ayarlar=None, include_duration=False):
    ayarlar = ayarlar or ayarlari_oku()
    sayilar = muzik_sayilari()
    playlist_ayarlar = ayarlar.get("playlists", {})
    playlist_order = [key for key in ayarlar.get("playlist_order", []) if key != "all"]
    listeler = [{
        "key": "all",
        "name": "Tüm Müzikler",
        "count": sayilar.get("all", 0),
        "duration": sure_formatla(playlist_suresi_saniye("all")) if include_duration else "-",
        "shuffle": playlist_karisik_mi(ayarlar, "all"),
        "active": True,
        "description": "",
        "playback_type": "general",
        "playback_order": ayarlar.get("playlist_mode", "shuffle"),
        "source": "songs",
        "remote_url": "",
        "weight": "3",
        "interval_value": "1",
        "include_requests": False,
        "avoid_duplicates": True,
        "show_on_public": True,
        "hide_metadata": False,
        "interrupt_other": False,
        "scheduled_single_track": False,
        "merge": False,
        "prioritize_requests": False,
        "schedule": {"start_time": "", "end_time": "", "start_date": "", "end_date": "", "days": [], "once": False},
    }]
    klasorler = []
    if os.path.exists(MUZIK_YOLU):
        container_folders = set(ayarlar.get("playlist_container_folders", [AUTO_HOUR_PARENT]))
        klasorler = [ad for ad in os.listdir(MUZIK_YOLU) if os.path.isdir(os.path.join(MUZIK_YOLU, ad)) and ad not in container_folders]
        order_index = {key: index for index, key in enumerate(playlist_order)}
        tum_keyler = []
        for key in playlist_order:
            if key in playlist_ayarlar and key not in tum_keyler:
                tum_keyler.append(key)
        for key in playlist_ayarlar:
            if key not in tum_keyler:
                tum_keyler.append(key)
        for key in klasorler:
            if key not in tum_keyler:
                tum_keyler.append(key)
        tum_keyler = sorted(tum_keyler, key=lambda ad: (order_index.get(ad, len(order_index)), ad.lower()))
        for ad in tum_keyler:
            tam_yol = playlist_klasor_yolu(ad, ayarlar)
            if os.path.isdir(tam_yol):
                playlist_cfg = playlist_ayarlar.get(ad, {})
                listeler.append({
                    "key": ad,
                    "name": playlist_cfg.get("name") or ad,
                    "count": len(muzik_dosyalari(ad)),
                    "duration": sure_formatla(playlist_suresi_saniye(ad)) if include_duration else "-",
                    "shuffle": playlist_karisik_mi(ayarlar, ad),
                    "active": playlist_cfg.get("active", True),
                    "description": playlist_cfg.get("description", ""),
                    "playback_type": playlist_cfg.get("playback_type", "general"),
                    "playback_order": playlist_cfg.get("playback_order", "shuffle"),
                    "source": playlist_cfg.get("source", "songs"),
                    "remote_url": playlist_cfg.get("remote_url", ""),
                    "weight": playlist_cfg.get("weight", "3"),
                    "interval_value": playlist_cfg.get("interval_value", "1"),
                    "include_requests": playlist_cfg.get("include_requests", False),
                    "avoid_duplicates": playlist_cfg.get("avoid_duplicates", True),
                    "show_on_public": playlist_cfg.get("show_on_public", True),
                    "hide_metadata": playlist_cfg.get("hide_metadata", False),
                    "interrupt_other": playlist_cfg.get("interrupt_other", False),
                    "scheduled_single_track": playlist_cfg.get("scheduled_single_track", False),
                    "merge": playlist_cfg.get("merge", False),
                    "prioritize_requests": playlist_cfg.get("prioritize_requests", False),
                    "jingle_group": playlist_cfg.get("jingle_group", False),
                    "manual_tracks": playlist_cfg.get("manual_tracks", []),
                    "display_group": playlist_cfg.get("display_group", "auto"),
                    "folder_path": playlist_cfg.get("folder_path", ad),
                    "schedule": playlist_cfg.get("schedule", {"start_time": "", "end_time": "", "start_date": "", "end_date": "", "days": [], "once": False}),
                })
    return listeler


def playlist_son_calma_bilgisi(liste, history=None):
    if liste.get("key") == "all":
        return {}
    history = history if history is not None else gecmis_oku()
    key = str(liste.get("key", "")).replace("\\", "/")
    name = str(liste.get("name", "")).strip()
    muzik_root = MUZIK_YOLU.replace("\\", "/")
    folder_rel = str(liste.get("folder_path") or key).replace("\\", "/").strip("/")
    folder_prefix = f"{muzik_root}/{folder_rel}/"
    track_paths = {str(path).replace("\\", "/") for path in liste.get("manual_tracks", [])}
    for item in history:
        filename = str(item.get("filename", "")).replace("\\", "/")
        title = str(item.get("title", "")).strip()
        if filename.startswith(folder_prefix) or filename in track_paths or (name and title == name):
            return {
                "time": item.get("time", ""),
                "title": item.get("title", "") or os.path.basename(filename),
                "artist": item.get("artist", ""),
                "filename": filename,
            }
    return {}


def saat_anonsu_playlisti_mi(liste):
    if liste.get("key") == "all" or liste.get("playback_type") != "once_per_hour":
        return False
    tracks = [str(path).replace("\\", "/").lower() for path in liste.get("manual_tracks", [])]
    if any("/jingle/saat/" in path for path in tracks):
        return True
    name_key = f'{liste.get("name", "")} {liste.get("key", "")}'.lower()
    return "saat" in name_key and bool(liste.get("jingle_group") or liste.get("scheduled_single_track"))


def normal_anons_playlisti_mi(liste):
    if liste.get("key") == "all" or saat_anonsu_playlisti_mi(liste):
        return False
    if not (liste.get("jingle_group") or liste.get("scheduled_single_track")):
        return False
    tracks = [str(path).replace("\\", "/").lower() for path in liste.get("manual_tracks", [])]
    return bool(tracks) and any("/jingle/" in path for path in tracks)


def playlist_gorunum_grubu(liste):
    secim = liste.get("display_group", "auto")
    if secim in {"main", "normal", "saat"}:
        return secim
    if saat_anonsu_playlisti_mi(liste):
        return "saat"
    if normal_anons_playlisti_mi(liste):
        return "normal"
    return "main"


def jingle_playlistleri(ayarlar=None):
    ayarlar = ayarlar or ayarlari_oku()
    saat_anonsu_dosya_baglarini_duzelt(ayarlar)
    sonuc = []
    for liste in muzik_listeleri(ayarlar):
        if liste["key"] == "all":
            continue
        is_jingle = bool(liste.get("jingle_group")) or "jingle" in liste.get("key", "").lower() or "jingle" in liste.get("name", "").lower()
        cfg = ayarlar.get("playlists", {}).get(liste["key"], {})
        if cfg.get("jingle_group"):
            is_jingle = True
        if not is_jingle:
            continue
        liste["jingle_group"] = True
        liste["manual_tracks"] = cfg.get("manual_tracks", [])
        if liste["manual_tracks"]:
            liste["count"] = sum(1 for path in liste["manual_tracks"] if os.path.exists(path))
        liste["preview_url"] = medya_onizleme_url(liste["manual_tracks"][0]) if liste["manual_tracks"] else ""
        sonuc.append(liste)

    def siralama_anahtari(liste):
        schedule = liste.get("schedule") or {}
        start = str(schedule.get("start_time") or "")
        try:
            saat = dt.time.fromisoformat(start)
            dakika = saat.hour * 60 + saat.minute
        except ValueError:
            dakika = 24 * 60 + 1
        if saat_anonsu_playlisti_mi(liste):
            grup = 0
        elif normal_anons_playlisti_mi(liste):
            grup = 1
        else:
            grup = 2
        return (grup, dakika, str(liste.get("name") or "").lower())

    return sorted(sonuc, key=siralama_anahtari)


def saat_dakika_degeri(value, fallback=24 * 60 + 1):
    try:
        parsed = dt.time.fromisoformat(str(value or ""))
        return parsed.hour * 60 + parsed.minute
    except ValueError:
        return fallback


def yayin_akisi_olustur(ayarlar=None, limit=160):
    ayarlar = ayarlar or ayarlari_oku()
    items = []
    today = dt.datetime.now(ISTANBUL_TZ)
    gun = str(today.weekday())
    for key, playlist in ayarlar.get("playlists", {}).items():
        liste = {"key": key, **playlist}
        if not (liste.get("jingle_group") or saat_anonsu_playlisti_mi(liste)):
            continue
        schedule = playlist.get("schedule") or {}
        days = [str(day) for day in schedule.get("days", [])]
        if days and gun not in days:
            continue
        start = schedule.get("start_time") or ""
        if not start:
            continue
        manual_tracks = playlist.get("manual_tracks", [])
        items.append({
            "time": start,
            "end": schedule.get("end_time") or "",
            "title": playlist.get("name") or key,
            "type": "Saat anonsu" if saat_anonsu_playlisti_mi(liste) else "Jingle",
            "status": "Aktif" if playlist.get("active", True) else "Pasif",
            "detail": f"{len(manual_tracks)} bağlı dosya" if manual_tracks else "",
        })
    for rule in ayarlar.get("schedules", []):
        days = [str(day) for day in rule.get("days", [])]
        if days and gun not in days:
            continue
        if not rule.get("time"):
            continue
        items.append({
            "time": rule.get("time", ""),
            "end": "",
            "title": rule.get("name") or rule.get("playlist") or "Planlı yayın",
            "type": "Takvim",
            "status": "Aktif" if rule.get("active", True) else "Pasif",
            "detail": rule.get("playlist", ""),
        })
    for relay in ayarlar.get("live_relays", []):
        if not relay.get("start_time"):
            continue
        items.append({
            "time": relay.get("start_time", ""),
            "end": relay.get("end_time", ""),
            "title": relay.get("name") or "Naklen yayın",
            "type": "Naklen",
            "status": "Aktif" if relay.get("active", True) else "Pasif",
            "detail": relay.get("url", ""),
        })
    items.sort(key=lambda item: (saat_dakika_degeri(item.get("time")), item.get("type", ""), item.get("title", "").lower()))
    minute_counts = Counter(item.get("time") for item in items if item.get("time"))
    for item in items:
        item["conflict"] = minute_counts.get(item.get("time"), 0) > 1
    return items[:limit]


def panel_uyarilari(ayarlar=None):
    ayarlar = ayarlar or ayarlari_oku()
    warnings = []
    if container_durumu() != "YAYINDA":
        warnings.append({"level": "danger", "title": "Yayın motoru kapalı", "text": "Ana panelden yayını başlatabilirsin."})
    saat_anonsu_dosya_baglarini_duzelt(ayarlar)
    for key, playlist in ayarlar.get("playlists", {}).items():
        if not playlist.get("active", True):
            continue
        liste = {"key": key, **playlist}
        if not (playlist.get("jingle_group") or saat_anonsu_playlisti_mi(liste)):
            continue
        manual_tracks = playlist.get("manual_tracks", [])
        missing = [path for path in manual_tracks if path and not os.path.exists(path)]
        if not manual_tracks:
            warnings.append({"level": "warning", "title": f"{playlist.get('name') or key} dosyasız", "text": "Aktif jingle/anons grubunda bağlı ses dosyası yok."})
        elif missing:
            warnings.append({"level": "warning", "title": f"{playlist.get('name') or key} eksik dosya", "text": f"{len(missing)} bağlı dosya bulunamadı."})
    if ayarlar.get("auto_backup_enabled"):
        last = ayarlar.get("last_auto_backup", "")
        today = dt.datetime.now(ISTANBUL_TZ).strftime("%Y-%m-%d")
        if last != today:
            warnings.append({"level": "info", "title": "Otomatik yedek bekliyor", "text": "Panel açıldığında günlük ayar yedeği alınır."})
    elif not yedek_listesi():
        warnings.append({"level": "info", "title": "Yedek yok", "text": "Ayarları korumak için Yedek sayfasından yedek alabilirsin."})
    return warnings[:8]


def listener_grafik_verisi(history):
    data = history[-96:]
    labels = []
    values = []
    for item in data:
        labels.append(str(item.get("time", ""))[-5:])
        try:
            values.append(int(item.get("listeners", 0) or 0))
        except (TypeError, ValueError):
            values.append(0)
    peak = max(values) if values else 0
    avg = round(sum(values) / len(values), 1) if values else 0
    return {"labels": labels, "values": values, "peak": peak, "avg": avg}


def playlist_key_uret(name):
    base = secure_filename(name).strip("_-.").lower()
    base = re.sub(r"[-\s]+", "_", base)
    if not base:
        base = "calma_listesi"
    key = base
    index = 2
    while os.path.exists(os.path.join(MUZIK_YOLU, key)):
        key = f"{base}_{index}"
        index += 1
    return key


def playlist_form_kaydi(playlist_key, existing=None):
    existing = existing or {}
    schedule = {
        "start_time": request.form.get("start_time", ""),
        "end_time": request.form.get("end_time", ""),
        "start_date": request.form.get("start_date", ""),
        "end_date": request.form.get("end_date", ""),
        "days": request.form.getlist("days"),
        "once": request.form.get("once") == "on",
    }
    return {
        "id": existing.get("id") or str(uuid.uuid4()),
        "key": playlist_key,
        "name": request.form.get("name", "").strip() or existing.get("name") or playlist_key,
        "active": request.form.get("active") == "on",
        "description": request.form.get("description", "").strip(),
        "source": request.form.get("source", "songs"),
        "remote_url": request.form.get("remote_url", "").strip(),
        "playback_type": request.form.get("playback_type", "general"),
        "playback_order": request.form.get("playback_order", "shuffle"),
        "weight": request.form.get("weight", "3"),
        "interval_value": request.form.get("interval_value", "1"),
        "include_requests": request.form.get("include_requests") == "on",
        "avoid_duplicates": request.form.get("avoid_duplicates") == "on",
        "show_on_public": request.form.get("show_on_public") == "on",
        "hide_metadata": request.form.get("hide_metadata") == "on",
        "interrupt_other": request.form.get("interrupt_other") == "on",
        "scheduled_single_track": request.form.get("scheduled_single_track") == "on",
        "merge": request.form.get("merge") == "on",
        "prioritize_requests": request.form.get("prioritize_requests") == "on",
        "schedule": schedule,
        "manual_tracks": existing.get("manual_tracks", []),
        "display_group": existing.get("display_group", "auto"),
        "folder_path": existing.get("folder_path", playlist_key),
        "created_at": existing.get("created_at") or dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
        "updated_at": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
    }


def benzersiz_dosya_yolu(folder, filename):
    safe_name = secure_filename(filename)
    if not safe_name:
        return ""
    base, ext = os.path.splitext(safe_name)
    hedef = os.path.join(folder, safe_name)
    index = 2
    while os.path.exists(hedef):
        hedef = os.path.join(folder, f"{base}-{index}{ext}")
        index += 1
    return hedef


_EDITOR_DOSYA_CACHE = {}  # playlist_key -> (zaman, [(path, rel_name)])
_EDITOR_DOSYA_TTL = 300.0  # saniye (5 dakika)

def _editor_tum_dosyalar(playlist_key):
    """Dosya listesini cache'li olarak doner."""
    import time as _time
    now = _time.monotonic()
    cached = _EDITOR_DOSYA_CACHE.get(playlist_key)
    if cached and now - cached[0] < _EDITOR_DOSYA_TTL:
        return cached[1]
    kok = MUZIK_YOLU if playlist_key == "all" else playlist_klasor_yolu(playlist_key)
    tum_dosyalar = []
    if os.path.exists(kok):
        for root, _, files in os.walk(kok):
            for file_name in sorted(files, key=str.lower):
                if not ses_dosyasi_mi(file_name):
                    continue
                path = rel_to_container_path(root, file_name)
                rel_name = os.path.relpath(os.path.join(root, file_name), MUZIK_YOLU).replace("\\", "/")
                tum_dosyalar.append((path, rel_name))
    _EDITOR_DOSYA_CACHE[playlist_key] = (_time.monotonic(), tum_dosyalar)
    return tum_dosyalar

def playlist_editor_dosyalari(playlist_key, ayarlar, page=1, page_size=80, query=""):
    banned = set(ayarlar.get("banned_tracks", []))
    favorites = set(ayarlar.get("favorite_tracks", []))
    page = max(1, int(page or 1))
    page_size = max(20, min(200, int(page_size or 80)))
    query = (query or "").strip().lower()
    kok = MUZIK_YOLU if playlist_key == "all" else playlist_klasor_yolu(playlist_key, ayarlar)
    if not os.path.exists(kok):
        return {"tracks": [], "page": 1, "page_size": page_size, "total": 0, "total_pages": 1, "query": query}
    tum = _editor_tum_dosyalar(playlist_key)
    if query:
        tum = [(p, r) for p, r in tum if query in r.lower()]
    total = len(tum)
    total_pages = max(1, (total + page_size - 1) // page_size)
    page = min(page, total_pages)
    offset = (page - 1) * page_size
    tracks = [
        {"path": p, "name": r, "favorite": p in favorites, "banned": p in banned}
        for p, r in tum[offset:offset + page_size]
    ]
    return {
        "tracks": tracks,
        "page": page,
        "page_size": page_size,
        "total": total,
        "total_pages": total_pages,
        "query": query,
    }


def temiz_sarki_anahtari(file_name):
    name = os.path.splitext(os.path.basename(file_name))[0].lower()
    name = re.sub(r"\([^)]*\)|\[[^]]*\]", " ", name)
    name = re.sub(r"\b(official|video|audio|lyrics?|remaster(ed)?|radio edit|extended mix)\b", " ", name)
    name = re.sub(r"[^a-z0-9ğüşöçıİĞÜŞÖÇ]+", " ", name, flags=re.IGNORECASE)
    return " ".join(name.split())


_HAVUZ_ANALIZI_CACHE = [None, 0.0]  # [sonuc, zaman]
_HAVUZ_ANALIZI_TTL = 300.0  # saniye (5 dakika)

def sarki_havuzu_analizi(limit=1200):
    import time as _time
    now = _time.monotonic()
    if _HAVUZ_ANALIZI_CACHE[0] is not None and now - _HAVUZ_ANALIZI_CACHE[1] < _HAVUZ_ANALIZI_TTL:
        return _HAVUZ_ANALIZI_CACHE[0]
    files = []
    extension_counts = Counter()
    folder_counts = Counter()
    duplicate_map = {}
    small_files = []
    large_files = []
    total_size = 0
    if os.path.exists(MUZIK_YOLU):
        for root, _, names in os.walk(MUZIK_YOLU):
            for file_name in sorted(names, key=str.lower):
                if not ses_dosyasi_mi(file_name):
                    continue
                full_path = os.path.join(root, file_name)
                rel_name = os.path.relpath(full_path, MUZIK_YOLU).replace("\\", "/")
                try:
                    size = os.path.getsize(full_path)
                except OSError:
                    size = 0
                total_size += size
                extension_counts[os.path.splitext(file_name)[1].lower()] += 1
                folder = rel_name.split("/", 1)[0] if "/" in rel_name else "Ana klasor"
                folder_counts[folder] += 1
                key = temiz_sarki_anahtari(file_name)
                if key:
                    duplicate_map.setdefault(key, []).append(rel_name)
                if size and size < 1024 * 1024:
                    small_files.append({"name": rel_name, "size_mb": round(size / 1024 / 1024, 2)})
                if size > 30 * 1024 * 1024:
                    large_files.append({"name": rel_name, "size_mb": round(size / 1024 / 1024, 1)})
                files.append(rel_name)
                if len(files) >= limit:
                    break
            if len(files) >= limit:
                break
    duplicates = [
        {"name": key, "items": values[:5], "count": len(values)}
        for key, values in duplicate_map.items()
        if len(values) > 1
    ]
    duplicates.sort(key=lambda item: item["count"], reverse=True)
    sonuc = {
        "scanned": len(files),
        "total_size_gb": round(total_size / 1024 / 1024 / 1024, 2),
        "extensions": extension_counts.most_common(8),
        "folders": folder_counts.most_common(8),
        "duplicates": duplicates[:8],
        "small_files": small_files[:8],
        "large_files": large_files[:8],
        "limited": len(files) >= limit,
    }
    _HAVUZ_ANALIZI_CACHE[0] = sonuc
    _HAVUZ_ANALIZI_CACHE[1] = _time.monotonic()
    return sonuc


def muzik_cache_temizle():
    """Dosya eklenince/silinince her iki cache'i de sifirla."""
    _EDITOR_DOSYA_CACHE.clear()
    _HAVUZ_ANALIZI_CACHE[0] = None


def jingle_dosyalari():
    dosyalar = []
    if os.path.exists(JINGLE_YOLU):
        for root, _, files in os.walk(JINGLE_YOLU):
            for file_name in sorted(files):
                if ses_dosyasi_mi(file_name):
                    tam_yol = rel_to_container_path(root, file_name)
                    dosyalar.append({
                        "path": tam_yol,
                        "name": os.path.relpath(tam_yol, JINGLE_YOLU).replace("\\", "/")
                    })
    return dosyalar


def haber_kaynaklari():
    return [
        {
            "key": "trt",
            "name": "TRT Haber",
            "url": "https://www.trthaber.com/sondakika_articles.rss",
        },
        {
            "key": "bbc",
            "name": "BBC Türkçe",
            "url": "https://feeds.bbci.co.uk/turkce/rss.xml",
        },
        {
            "key": "aposto",
            "name": "Aposto (RSS adresini özel alana gir)",
            "url": "",
        },
        {
            "key": "custom",
            "name": "Özel RSS adresi",
            "url": "",
        },
    ]


def haber_podcast_kaynaklari():
    return [
        {
            "key": "kisa_dalga",
            "name": "Kısa Dalga Bülten",
            "url": "https://feeds.megaphone.fm/kisa-dalga-bulten",
            "note": "Günlük gündem bülteni, yaklaşık 12 dakika.",
        },
        {
            "key": "ntv_radyo",
            "name": "NTVRadyo",
            "url": "https://feeds.soundcloud.com/users/soundcloud:users:3005942/sounds.rss",
            "note": "NTV Radyo podcast akışı; İşe Giderken/Eve Dönerken gibi haber programları bulunur.",
        },
        {
            "key": "cumhuriyet",
            "name": "Cumhuriyet gazetesi",
            "url": "https://anchor.fm/s/f215ffc0/podcast/rss",
            "note": "Kısa haber özetleri, genelde 3-5 dakika.",
        },
        {
            "key": "custom",
            "name": "Özel podcast RSS adresi",
            "url": "",
            "note": "RSS içinde MP3 enclosure olan özel bir kaynak gir.",
        },
    ]


def haber_varsayilan_ayarlar():
    return {
        "enabled": False,
        "source": "trt",
        "custom_rss": "",
        "item_count": 5,
        "times": "08:00\n12:00\n18:00",
        "intro_jingle": "",
        "outro_jingle": "",
        "background_music": "",
        "background_volume": 18,
        "voice_provider": "text_tts",
        "edge_voice": "tr-TR-EmelNeural",
        "openai_api_key": "",
        "openai_model": "gpt-4o-mini-tts",
        "openai_voice": "alloy",
        "elevenlabs_api_key": "",
        "elevenlabs_voice_id": "",
        "podcast_source": "kisa_dalga",
        "podcast_rss": "",
        "cleanup_days": 14,
        "last_generated": "",
        "last_audio": "",
        "last_audio_name": "",
        "last_error": "",
    }


def haber_ayarlari_oku():
    defaults = haber_varsayilan_ayarlar()
    if not os.path.exists(HABER_AYARLAR_YOLU):
        return defaults
    try:
        with open(HABER_AYARLAR_YOLU, "r", encoding="utf-8") as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError):
        return defaults
    for key, value in defaults.items():
        data.setdefault(key, value)
    return data


def haber_ayarlari_yaz(ayarlar):
    os.makedirs(STATE_YOLU, exist_ok=True)
    with open(HABER_AYARLAR_YOLU, "w", encoding="utf-8") as f:
        json.dump(ayarlar, f, ensure_ascii=False, indent=2)


def haber_rss_url(ayarlar):
    if ayarlar.get("source") == "custom":
        return ayarlar.get("custom_rss", "").strip()
    kaynak = next((item for item in haber_kaynaklari() if item["key"] == ayarlar.get("source")), None)
    if kaynak and kaynak.get("url"):
        return kaynak["url"]
    return ayarlar.get("custom_rss", "").strip()


def haber_podcast_rss_url(ayarlar):
    if ayarlar.get("podcast_source") != "custom":
        kaynak = next((item for item in haber_podcast_kaynaklari() if item["key"] == ayarlar.get("podcast_source")), None)
        if kaynak and kaynak.get("url"):
            return kaynak["url"]
    return ayarlar.get("podcast_rss", "").strip()


def metin_temizle(text, limit=360):
    text = html.unescape(re.sub(r"<[^>]+>", " ", text or ""))
    text = re.sub(r"\s+", " ", text).strip()
    if len(text) > limit:
        text = text[:limit].rsplit(" ", 1)[0].strip() + "..."
    return text


def haber_rss_oku(ayarlar, limit=None):
    url = haber_rss_url(ayarlar)
    if not url:
        return [], "RSS adresi yok."
    limit = limit or int(ayarlar.get("item_count", 5) or 5)
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "Akkurtcast/1.0"})
        with urllib.request.urlopen(req, timeout=8) as response:
            raw = response.read(1024 * 1024)
        root = ET.fromstring(raw)
    except Exception as exc:
        return [], f"RSS okunamadı: {exc}"

    items = []
    for item in root.findall(".//item")[:limit]:
        title = metin_temizle(item.findtext("title"), 180)
        desc = metin_temizle(item.findtext("description"), 420)
        link = metin_temizle(item.findtext("link"), 400)
        pub_date = metin_temizle(item.findtext("pubDate"), 120)
        if title:
            items.append({"title": title, "summary": desc, "link": link, "pub_date": pub_date})

    if not items:
        for entry in root.findall(".//{http://www.w3.org/2005/Atom}entry")[:limit]:
            title = metin_temizle(entry.findtext("{http://www.w3.org/2005/Atom}title"), 180)
            desc = metin_temizle(entry.findtext("{http://www.w3.org/2005/Atom}summary"), 420)
            if title:
                items.append({"title": title, "summary": desc, "link": "", "pub_date": ""})
    return items, ""


def haber_podcast_rss_oku(ayarlar, limit=8):
    url = haber_podcast_rss_url(ayarlar) or haber_rss_url(ayarlar)
    if not url:
        return [], "Sesli RSS adresi yok."
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "Akkurtcast/1.0"})
        with urllib.request.urlopen(req, timeout=10) as response:
            raw = response.read(32 * 1024 * 1024)
        root = ET.fromstring(raw)
    except Exception as exc:
        return [], f"Sesli RSS okunamadı: {exc}"

    items = []
    media_ns = "{http://search.yahoo.com/mrss/}"
    for item in root.findall(".//item"):
        title = metin_temizle(item.findtext("title"), 180) or "Sesli haber"
        desc = metin_temizle(item.findtext("description"), 300)
        audio_url = ""
        audio_type = ""
        enclosure = item.find("enclosure")
        if enclosure is not None:
            audio_url = enclosure.attrib.get("url", "")
            audio_type = enclosure.attrib.get("type", "")
        if not audio_url:
            media = item.find(f"{media_ns}content")
            if media is not None:
                audio_url = media.attrib.get("url", "")
                audio_type = media.attrib.get("type", "")
        if audio_url and ("audio" in audio_type or audio_url.lower().split("?")[0].endswith(SES_UZANTILARI)):
            items.append({"title": title, "summary": desc, "audio_url": audio_url, "type": audio_type})
            if len(items) >= limit:
                break

    atom_ns = "{http://www.w3.org/2005/Atom}"
    if not items:
        for entry in root.findall(f".//{atom_ns}entry"):
            title = metin_temizle(entry.findtext(f"{atom_ns}title"), 180) or "Sesli haber"
            for link in entry.findall(f"{atom_ns}link"):
                href = link.attrib.get("href", "")
                link_type = link.attrib.get("type", "")
                rel = link.attrib.get("rel", "")
                if href and (rel == "enclosure" or "audio" in link_type or href.lower().split("?")[0].endswith(SES_UZANTILARI)):
                    items.append({"title": title, "summary": "", "audio_url": href, "type": link_type})
                    break
            if len(items) >= limit:
                break
    return items, "" if items else "Bu RSS içinde ses dosyası bulunamadı."


def haber_bulteni_metni_olustur(items):
    saat = dt.datetime.now(ISTANBUL_TZ).strftime("%H:%M")
    satirlar = [f"Radyo Pop haber bülteni. Saat {saat}. Gündemde öne çıkan başlıklar."]
    for index, item in enumerate(items, start=1):
        title = item.get("title", "").strip()
        summary = item.get("summary", "").strip()
        if summary:
            satirlar.append(f"{index}. {title}. {summary}")
        else:
            satirlar.append(f"{index}. {title}.")
    satirlar.append("Haber bülteni sona erdi. Radyo Pop ile müzik devam ediyor.")
    return "\n\n".join(satirlar)


def haber_cikti_dosyalari():
    dosyalar = []
    if os.path.exists(HABER_CIKTI_YOLU):
        for name in sorted(os.listdir(HABER_CIKTI_YOLU), reverse=True):
            path = os.path.join(HABER_CIKTI_YOLU, name)
            if os.path.isfile(path) and ses_dosyasi_mi(name):
                dosyalar.append({
                    "name": name,
                    "path": rel_to_container_path(HABER_CIKTI_YOLU, name),
                    "size": os.path.getsize(path),
                })
    return dosyalar


def haber_indirilebilir_ad(title, ext=".mp3"):
    base = secure_filename(metin_temizle(title, 80)) or "haber-bulteni"
    stamp = dt.datetime.now(ISTANBUL_TZ).strftime("%Y%m%d-%H%M%S")
    return f"{stamp}-{base}{ext}"


def haber_audio_indir(audio_url, title):
    os.makedirs(HABER_CIKTI_YOLU, exist_ok=True)
    parsed = urllib.parse.urlparse(audio_url)
    ext = os.path.splitext(parsed.path)[1].lower()
    if ext not in SES_UZANTILARI:
        ext = ".mp3"
    out_path = os.path.join(HABER_CIKTI_YOLU, haber_indirilebilir_ad(title, ext))
    req = urllib.request.Request(audio_url, headers={"User-Agent": "Akkurtcast/1.0"})
    with urllib.request.urlopen(req, timeout=30) as response, open(out_path, "wb") as f:
        shutil.copyfileobj(response, f)
    return out_path


def haber_tts_uret_edge(metin, ayarlar):
    os.makedirs(HABER_CIKTI_YOLU, exist_ok=True)
    out_path = os.path.join(HABER_CIKTI_YOLU, haber_indirilebilir_ad("spiker-sesi", ".mp3"))
    voice = ayarlar.get("edge_voice") or "tr-TR-EmelNeural"
    subprocess.run(
        ["edge-tts", "--voice", voice, "--text", metin, "--write-media", out_path],
        check=True,
        capture_output=True,
        text=True,
        timeout=180,
    )
    return out_path


def haber_tts_uret_openai(metin, ayarlar):
    api_key = ayarlar.get("openai_api_key") or os.environ.get("OPENAI_API_KEY", "")
    if not api_key:
        raise RuntimeError("OpenAI API anahtarı girilmedi.")
    os.makedirs(HABER_CIKTI_YOLU, exist_ok=True)
    out_path = os.path.join(HABER_CIKTI_YOLU, haber_indirilebilir_ad("openai-spiker", ".mp3"))
    payload = {
        "model": ayarlar.get("openai_model") or "gpt-4o-mini-tts",
        "voice": ayarlar.get("openai_voice") or "alloy",
        "input": metin,
        "response_format": "mp3",
    }
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        "https://api.openai.com/v1/audio/speech",
        data=data,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "User-Agent": "Akkurtcast/1.0",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=180) as response, open(out_path, "wb") as f:
            shutil.copyfileobj(response, f)
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="ignore")
        raise RuntimeError(f"OpenAI ses üretimi başarısız: {exc.code} {detail[:300]}") from exc
    return out_path


def haber_tts_uret_elevenlabs(metin, ayarlar):
    api_key = ayarlar.get("elevenlabs_api_key") or os.environ.get("ELEVENLABS_API_KEY", "")
    voice_id = ayarlar.get("elevenlabs_voice_id", "").strip()
    if not api_key:
        raise RuntimeError("ElevenLabs API anahtarı girilmedi.")
    if not voice_id:
        raise RuntimeError("ElevenLabs Voice ID girilmedi.")
    os.makedirs(HABER_CIKTI_YOLU, exist_ok=True)
    out_path = os.path.join(HABER_CIKTI_YOLU, haber_indirilebilir_ad("elevenlabs-spiker", ".mp3"))
    payload = {
        "text": metin,
        "model_id": "eleven_multilingual_v2",
        "voice_settings": {"stability": 0.55, "similarity_boost": 0.75},
    }
    data = json.dumps(payload).encode("utf-8")
    url = f"https://api.elevenlabs.io/v1/text-to-speech/{urllib.parse.quote(voice_id)}?output_format=mp3_44100_128"
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "xi-api-key": api_key,
            "Content-Type": "application/json",
            "Accept": "audio/mpeg",
            "User-Agent": "Akkurtcast/1.0",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=180) as response, open(out_path, "wb") as f:
            shutil.copyfileobj(response, f)
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="ignore")
        raise RuntimeError(f"ElevenLabs ses üretimi başarısız: {exc.code} {detail[:300]}") from exc
    return out_path


def haber_tts_uret(metin, ayarlar):
    provider = ayarlar.get("voice_provider") or "text_tts"
    if provider == "openai_tts":
        return haber_tts_uret_openai(metin, ayarlar)
    if provider == "elevenlabs_tts":
        return haber_tts_uret_elevenlabs(metin, ayarlar)
    return haber_tts_uret_edge(metin, ayarlar)


def haber_cikti_temizle(days=14, delete_all=False):
    if not os.path.isdir(HABER_CIKTI_YOLU):
        return 0
    now_ts = time.time()
    deleted = 0
    try:
        days = max(1, int(days or 14))
    except (TypeError, ValueError):
        days = 14
    cutoff = now_ts - (days * 86400)
    for name in os.listdir(HABER_CIKTI_YOLU):
        path = os.path.join(HABER_CIKTI_YOLU, name)
        if not os.path.isfile(path):
            continue
        if not ses_dosyasi_mi(name) and not name.endswith(".txt"):
            continue
        if delete_all or os.path.getmtime(path) < cutoff:
            try:
                os.remove(path)
                deleted += 1
            except OSError:
                pass
    return deleted


def ffmpeg_var_mi():
    try:
        subprocess.run(["ffmpeg", "-version"], capture_output=True, timeout=5, check=True)
        return True
    except Exception:
        return False


def haber_core_miksle(voice_path, ayarlar):
    bg = ayarlar.get("background_music", "")
    if not bg or not os.path.isfile(bg) or not ffmpeg_var_mi():
        return voice_path
    os.makedirs(HABER_CIKTI_YOLU, exist_ok=True)
    mixed = os.path.join(HABER_CIKTI_YOLU, haber_indirilebilir_ad("fonlu-spiker", ".mp3"))
    volume = max(0, min(60, int(ayarlar.get("background_volume", 18) or 18))) / 100
    subprocess.run(
        [
            "ffmpeg", "-y",
            "-i", voice_path,
            "-stream_loop", "-1", "-i", bg,
            "-filter_complex", f"[1:a]volume={volume}[bg];[0:a][bg]amix=inputs=2:duration=first:dropout_transition=2[mix]",
            "-map", "[mix]",
            "-codec:a", "libmp3lame",
            "-q:a", "4",
            mixed,
        ],
        check=True,
        capture_output=True,
        text=True,
        timeout=180,
    )
    return mixed


def haber_final_miksle(core_path, ayarlar, title="haber-bulteni"):
    os.makedirs(HABER_CIKTI_YOLU, exist_ok=True)
    parts = []
    for key in ("intro_jingle",):
        path = ayarlar.get(key, "")
        if path and os.path.isfile(path):
            parts.append(path)
    parts.append(core_path)
    for key in ("outro_jingle",):
        path = ayarlar.get(key, "")
        if path and os.path.isfile(path):
            parts.append(path)
    final_path = os.path.join(HABER_CIKTI_YOLU, haber_indirilebilir_ad(title, ".mp3"))
    if len(parts) == 1 or not ffmpeg_var_mi():
        shutil.copyfile(core_path, final_path)
        return final_path
    concat_path = os.path.join(HABER_CIKTI_YOLU, f"concat-{uuid.uuid4().hex}.txt")
    with open(concat_path, "w", encoding="utf-8") as f:
        for path in parts:
            safe_path = path.replace("'", "'\\''")
            f.write(f"file '{safe_path}'\n")
    try:
        subprocess.run(
            ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_path, "-codec:a", "libmp3lame", "-q:a", "4", final_path],
            check=True,
            capture_output=True,
            text=True,
            timeout=180,
        )
    finally:
        try:
            os.remove(concat_path)
        except OSError:
            pass
    return final_path


def haber_bulteni_sesli_uret(ayarlar):
    if ayarlar.get("voice_provider") == "audio_rss":
        items, error = haber_podcast_rss_oku(ayarlar, limit=1)
        if error or not items:
            raise RuntimeError(error or "Sesli haber bulunamadı.")
        source_audio = haber_audio_indir(items[0]["audio_url"], items[0]["title"])
        title = items[0]["title"]
    else:
        metin = haber_taslak_oku()
        if not metin.strip():
            items, error = haber_rss_oku(ayarlar)
            if error or not items:
                raise RuntimeError(error or "Haber metni üretilemedi.")
            metin = haber_bulteni_metni_olustur(items)
            with open(HABER_TASLAK_YOLU, "w", encoding="utf-8") as f:
                f.write(metin)
        source_audio = haber_tts_uret(metin, ayarlar)
        title = "Radyo Pop Haber Bülteni"
    core = haber_core_miksle(source_audio, ayarlar)
    final_path = haber_final_miksle(core, ayarlar, title)
    return final_path


def haber_muzikleri():
    dosyalar = []
    if os.path.exists(HABER_YOLU):
        for root, _, files in os.walk(HABER_YOLU):
            for file_name in sorted(files, key=str.lower):
                if ses_dosyasi_mi(file_name):
                    tam_yol = rel_to_container_path(root, file_name)
                    dosyalar.append({
                        "path": tam_yol,
                        "name": os.path.relpath(tam_yol, HABER_YOLU).replace("\\", "/"),
                    })
    return dosyalar


def haber_taslak_oku():
    if not os.path.exists(HABER_TASLAK_YOLU):
        return ""
    try:
        with open(HABER_TASLAK_YOLU, "r", encoding="utf-8") as f:
            return f.read()
    except OSError:
        return ""


def show_haber_varsayilan_ayarlar():
    return {
        "timezone": "Europe/Istanbul",
        "stream_url": "https://windows.showradyo.com.tr",
        "schedule_times": ["08:00", "09:00", "10:00", "12:00", "15:00", "17:00", "19:00"],
        "enabled_weekdays": [1, 2, 3, 4, 5, 6, 7],
        "start_padding_seconds": 8,
        "auto_trim_leading_audio": True,
        "pre_speech_keep_seconds": 4,
        "start_ignore_phrases": [
            "açarız radyoyu",
            "acariz radyoyu",
            "yollere biz oraya",
        ],
        "output_trim_start_seconds": 0,
        "max_duration_seconds": 240,
        "require_stop_phrase": True,
        "chunk_seconds": 15,
        "min_elapsed_before_stop_seconds": 60,
        "output_dir": "/var/lib/show-haber-recorder/recordings",
        "work_dir": "/var/lib/show-haber-recorder/work",
        "state_file": "/var/lib/show-haber-recorder/state.json",
        "trigger_file": "/root/radyo-projesi/state/show_haber_trigger.json",
        "lock_file": "/var/lib/show-haber-recorder/run.lock",
        "log_file": "/var/log/show-haber-recorder/recorder.log",
        "transcription": {
            "engine": "faster_whisper",
            "model": "small",
            "device": "cpu",
            "compute_type": "int8",
            "language": "tr",
        },
        "stop_phrases": [
            "pilot garaj yol durumunu sundu",
            "pilot garaj yoldurumunu sundu",
            "pilot garaj ekspertiz",
            "pilot garaj expertiz",
            "pilot garanç yoldurumunu sundu",
            "pilot garanç oto ekspertiz",
            "pilot garanç ota ekspert",
        ],
        "stop_required_terms": [],
        "stop_extra_seconds": 0.4,
        "stop_phrase_extra_seconds": {
            "pilot garaj yol durumunu sundu": 1.8,
            "pilot garaj yoldurumunu sundu": 1.8,
            "pilot garanç yoldurumunu sundu": 1.8,
        },
        "wrap_audio": {
            "enabled": False,
            "intro_path": "",
            "intro_max_seconds": 0,
            "outro_path": "",
            "outro_max_seconds": 0,
            "trim_silence": True,
            "silence_threshold": "-45dB",
            "crossfade_seconds": 0.35,
            "intro_crossfade_seconds": 1.5,
            "outro_crossfade_seconds": 0.35,
        },
        "upload": {
            "enabled": True,
            "method": "sftp",
            "host": "akkurtcastpanel.radyopop.site",
            "port": 2222,
            "username": "radyo",
            "password": "radyo123",
            "remote_dir": "/media/haber_bultenleri",
            "archive_dir": "/media/haber_bultenleri_arsiv",
            "latest_filename": "show-haber-son.mp3",
            "use_tls": False,
            "post_upload_command": "",
        },
        "panel": {"play_offset_minutes": 5, "play_times": ["08:05", "12:05", "13:05"]},
        "loudness_normalization": {
            "enabled": True,
            "mode": "boost_news",
            "target_i": -16.0,
            "news_target_i": -11.0,
            "music_target_i": -11.0,
            "normalize_music": False,
            "target_tp": -1.0,
            "target_lra": 11.0,
        },
        "news_audio_profile": {
            "enabled": True,
            "highpass_hz": 85.0,
            "lowpass_hz": 15000.0,
            "low_mid_hz": 250.0,
            "low_mid_gain": -1.2,
            "presence_hz": 3200.0,
            "presence_gain": 2.4,
            "air_hz": 9000.0,
            "air_gain": 1.3,
            "compressor_enabled": True,
            "compressor_threshold": -18.0,
            "compressor_ratio": 2.2,
            "compressor_makeup": 1.5,
            "limiter_enabled": True,
            "limiter_limit": 0.98,
        },
    }


def show_haber_ayarlari_oku():
    defaults = show_haber_varsayilan_ayarlar()
    if not os.path.exists(SHOW_HABER_CONFIG_YOLU):
        return defaults
    try:
        with open(SHOW_HABER_CONFIG_YOLU, "r", encoding="utf-8") as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError):
        data = {}
    merged = defaults
    for key, value in data.items():
        if isinstance(value, dict) and isinstance(merged.get(key), dict):
            merged[key].update(value)
        else:
            merged[key] = value
    return merged


def show_haber_ayarlari_yaz(ayarlar):
    os.makedirs(STATE_YOLU, exist_ok=True)
    with open(SHOW_HABER_CONFIG_YOLU, "w", encoding="utf-8") as f:
        json.dump(ayarlar, f, ensure_ascii=False, indent=2)


def saat_listesi_parse(value):
    sonuc = []
    for item in re.split(r"[\s,;]+", value or ""):
        item = item.strip()
        if re.match(r"^\d{2}:\d{2}$", item) and item not in sonuc:
            sonuc.append(item)
    return sonuc


def saat_ustune_dakika_ekle(value, dakika=5):
    try:
        hour, minute = [int(part) for part in value.split(":", 1)]
    except (ValueError, AttributeError):
        hour, minute = 13, 0
    total = (hour * 60 + minute + int(dakika)) % (24 * 60)
    return f"{total // 60:02d}:{total % 60:02d}"


def show_haber_yayin_saatleri(schedule_times, offset_minutes=5):
    play_times = []
    for item in schedule_times or []:
        play_time = saat_ustune_dakika_ekle(item, offset_minutes)
        if play_time not in play_times:
            play_times.append(play_time)
    return play_times or ["08:05", "12:05", "13:05"]


def show_haber_playlist_guncelle(schedule_times, offset_minutes=5):
    play_times = show_haber_yayin_saatleri(schedule_times, offset_minutes)
    first_play_time = play_times[0]
    _, minute = first_play_time.split(":", 1)
    ayarlar = ayarlari_oku()
    playlists = ayarlar.setdefault("playlists", {})
    key = "haber_bultenleri"
    mevcut = playlists.get(key, {})
    playlist = {
        "id": mevcut.get("id") or str(uuid.uuid4()),
        "key": key,
        "name": "Haber Bültenleri",
        "active": True,
        "description": f"Show Radyo haber bülteni otomatik kaydı. Kayıttan {offset_minutes} dakika sonra yayınlanır.",
        "source": "songs",
        "remote_url": "",
        "playback_type": "once_per_hour",
        "playback_order": "normal",
        "weight": "3",
        "interval_value": str(int(minute)),
        "include_requests": False,
        "avoid_duplicates": False,
        "show_on_public": False,
        "hide_metadata": False,
        "interrupt_other": False,
        "scheduled_single_track": True,
        "merge": False,
        "prioritize_requests": False,
        "jingle_group": True,
        "manual_tracks": ["/web-radyo/muzik/haber_bultenleri/show-haber-son.mp3"],
        "schedule": {
            "start_time": "",
            "end_time": "",
            "start_date": "",
            "end_date": "",
            "days": [],
            "once": False,
            "times": play_times,
        },
        "display_group": "normal",
        "folder_path": "haber_bultenleri",
        "created_at": mevcut.get("created_at") or dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
        "updated_at": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
    }
    playlists[key] = playlist
    if key not in ayarlar.setdefault("playlist_order", []):
        ayarlar["playlist_order"].append(key)
    ayarlar.setdefault("playlist_shuffle", {})[key] = False
    containers = ayarlar.setdefault("playlist_container_folders", [AUTO_HOUR_PARENT])
    if "haber_bultenleri_arsiv" not in containers:
        containers.append("haber_bultenleri_arsiv")
    ayarlari_yaz(ayarlar)


def show_haber_saat_secenekleri():
    return [f"{hour:02d}:00" for hour in range(24)]


def saat_dakika_secenekleri():
    return {
        "hours": [f"{hour:02d}" for hour in range(24)],
        "minutes": [f"{minute:02d}" for minute in range(60)],
    }


def show_haber_durum_bilgisi():
    latest = os.path.join(MUZIK_YOLU, "haber_bultenleri", "show-haber-son.mp3")
    if not os.path.exists(latest):
        return {"latest_exists": False}
    stat = os.stat(latest)
    return {
        "latest_exists": True,
        "latest_path": "/web-radyo/muzik/haber_bultenleri/show-haber-son.mp3",
        "latest_url": "/media/muzik/haber_bultenleri/show-haber-son.mp3",
        "latest_size_mb": round(stat.st_size / (1024 * 1024), 2),
        "latest_mtime": dt.datetime.fromtimestamp(stat.st_mtime, ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
    }


def yuzde_secenekleri(minimum=-55, maksimum=55, adim=5):
    return list(range(minimum, maksimum + 1, adim))


def ses_yuzdesini_katsayi(value):
    try:
        yuzde = int(value or 0)
    except (TypeError, ValueError):
        yuzde = 0
    yuzde = max(-80, min(80, yuzde))
    return max(0.05, 1 + (yuzde / 100))


def panel_gorsel_kaydet(file, klasor):
    if not file or not file.filename:
        return ""
    ext = os.path.splitext(file.filename)[1].lower()
    if ext not in (".png", ".jpg", ".jpeg", ".gif", ".webp"):
        return ""
    upload_dir = os.path.join(app.static_folder, "uploads", klasor)
    os.makedirs(upload_dir, exist_ok=True)
    name = f"{uuid.uuid4().hex}{ext}"
    file.save(os.path.join(upload_dir, name))
    return f"/static/uploads/{klasor}/{name}"


def live_relay_bul(ayarlar, relay_id):
    return next((relay for relay in ayarlar.get("live_relays", []) if relay.get("id") == relay_id), None)


def icecast_metadata_guncelle(baslik):
    baslik = (baslik or "").strip()
    if not baslik:
        return False
    query = urllib.parse.urlencode({
        "mount": "/radyo",
        "mode": "updinfo",
        "song": baslik,
    })
    request_obj = urllib.request.Request(f"http://icecast-server:8000/admin/metadata?{query}")
    request_obj.add_header("Authorization", "Basic YWRtaW46aGFja21l")
    try:
        with urllib.request.urlopen(request_obj, timeout=2) as response:
            return response.status < 400
    except (urllib.error.URLError, TimeoutError, OSError):
        return False


def dj_bul(ayarlar, dj_id):
    return next((dj for dj in ayarlar.get("djs", []) if dj.get("id") == dj_id), None)


def dj_slotlari_hazirla(ayarlar):
    slots = ayarlar.setdefault("dj_slots", [])
    mevcut = {int(slot.get("port", 0) or 0): slot for slot in slots}
    for port in range(1290, 1306):
        if port not in mevcut:
            mevcut[port] = {
                "port": port,
                "mount": f"/dj-{port}",
                "password": str(uuid.uuid4()).replace("-", "")[:8],
            }
    ayarlar["dj_slots"] = [mevcut[port] for port in sorted(mevcut) if 1290 <= port <= 1305]
    return ayarlar["dj_slots"]


def dj_slot_bul(ayarlar, port):
    for slot in dj_slotlari_hazirla(ayarlar):
        if int(slot.get("port", 0) or 0) == int(port or 0):
            return slot
    return None


def dj_bos_slot(ayarlar):
    used = {int(dj.get("port", 0) or 0) for dj in ayarlar.get("djs", [])}
    for slot in dj_slotlari_hazirla(ayarlar):
        port = int(slot.get("port", 0) or 0)
        if port not in used:
            return slot
    return dj_slotlari_hazirla(ayarlar)[0]


def dj_source_id(dj):
    mount = (dj.get("mount") or "").strip().lstrip("/")
    if mount:
        return re.sub(r"[^A-Za-z0-9_-]", "-", mount).strip("-")[:48] or f"dj-{dj.get('port', '1290')}"
    base = str(dj.get("name") or "").strip().lower()
    tr_map = str.maketrans({"ç": "c", "ğ": "g", "ı": "i", "ö": "o", "ş": "s", "ü": "u"})
    base = base.translate(tr_map)
    slug = re.sub(r"[^a-z0-9]+", "-", base).strip("-")
    return f"dj-{slug or dj.get('port', '1290')}"[:48]


def live_relay_yayina_al(relay):
    url = (relay.get("url") or "").strip()
    if not url:
        return False
    client = guvenli_docker_client()
    if client is None:
        return False
    ayarlar = ayarlari_oku()
    slots = dj_slotlari_hazirla(ayarlar)
    slot = next((item for item in slots if int(item.get("port", 0) or 0) == 1305), slots[-1])
    network = container_network("radio-engine") or "radyo-projesi_default"
    name = "radio-live-relay"
    live_relay_durdur()
    relay_name = (relay.get("name") or "Naklen Yayın").strip()
    target = f"icecast://source:{slot.get('password')}@radio-engine:{slot.get('port')}{slot.get('mount')}"
    command = [
        "-reconnect", "1",
        "-reconnect_streamed", "1",
        "-reconnect_delay_max", "5",
        "-i", url,
        "-vn",
        "-c:a", "libmp3lame",
        "-b:a", "128k",
        "-content_type", "audio/mpeg",
        "-metadata", f"title={relay_name}",
        "-f", "mp3",
        target,
    ]
    try:
        client.containers.run(
            "jrottenberg/ffmpeg:6.1-alpine",
            command=command,
            name=name,
            detach=True,
            network=network,
            restart_policy={"Name": "no"},
        )
        icecast_metadata_guncelle(relay_name)
        return True
    except docker.errors.ImageNotFound:
        return False
    except docker.errors.DockerException as hata:
        print(f"Naklen yayin baslatma hatasi: {hata}")
        return False


def live_relay_durdur():
    client = guvenli_docker_client()
    if client is not None:
        try:
            container = client.containers.get("radio-live-relay")
            container.remove(force=True)
        except docker.errors.NotFound:
            pass
        except docker.errors.DockerException as hata:
            print(f"Naklen yayin durdurma hatasi: {hata}")
    icecast_metadata_guncelle(ayarlari_oku().get("station_name", "Radyo Pop"))


def m3u_yaz(dosyalar):
    os.makedirs(STATE_YOLU, exist_ok=True)
    with open(PLAYLIST_YOLU, "w", encoding="utf-8") as f:
        for dosya in dosyalar:
            artist, title = dosya_adindan_meta(dosya)
            annotations = [f'title="{liq_annotate_value(title)}"']
            if artist:
                annotations.append(f'artist="{liq_annotate_value(artist)}"')
            f.write("annotate:" + ",".join(annotations) + ":" + dosya + "\n")


def annotate_uri_dosya_yolu(uri):
    uri = (uri or "").strip()
    if not uri.startswith("annotate:"):
        return uri
    parts = uri.split(":")
    for index, part in enumerate(parts[1:], start=1):
        if part.startswith("/") or part.startswith("\\"):
            return ":".join(parts[index:])
    return uri


def m3u_oku():
    if not os.path.exists(PLAYLIST_YOLU):
        return []
    try:
        with open(PLAYLIST_YOLU, "r", encoding="utf-8") as f:
            return [annotate_uri_dosya_yolu(line.strip()) for line in f if line.strip() and not line.startswith("#")]
    except OSError:
        return []


def dosya_gorunum_yolu(path):
    normalized = (path or "").replace("\\", "/")
    muzik_root = MUZIK_YOLU.replace("\\", "/")
    jingle_root = JINGLE_YOLU.replace("\\", "/")
    try:
        if normalized.startswith(muzik_root):
            return os.path.relpath(normalized, muzik_root).replace("\\", "/")
        if normalized.startswith(jingle_root):
            return "jingle/" + os.path.relpath(normalized, jingle_root).replace("\\", "/")
    except ValueError:
        pass
    return os.path.basename(normalized)


def siradaki_sarki_bilgisi(now=None):
    playlist = [path.replace("\\", "/") for path in m3u_oku()]
    playlist = [path for path in playlist if path]
    if not playlist:
        return {}
    now = now or aktif_metadata()
    current = (now.get("filename") or "").replace("\\", "/")
    next_index = 0
    if current in playlist:
        next_index = (playlist.index(current) + 1) % len(playlist)
    next_path = playlist[next_index]
    artist, title = dosya_adindan_meta(next_path)
    return {
        "title": title or os.path.splitext(os.path.basename(next_path))[0],
        "artist": artist,
        "filename": next_path,
        "path": next_path,
        "relative": dosya_gorunum_yolu(next_path),
    }


def aktif_rid_dosya_yolu(aktif_rid):
    if not aktif_rid or ":" not in aktif_rid:
        return ""
    return aktif_rid.split(":", 1)[1]

def playlist_jingle_enjekte_et(jingle, aktif_dosya):
    paths = jingle_kuyruk_yollari(jingle)
    if not paths or not aktif_dosya:
        return False
    jingle_path = paths[0]
    ayarlar = ayarlari_oku()
    ek_id = jingle.get("id") or jingle.get("name") or jingle_path
    ekler = [ek for ek in ayarlar.get("scheduled_playlist_inserts", []) if ek.get("id") != ek_id]
    ekler.append({
        "id": ek_id,
        "name": jingle.get("name") or os.path.basename(jingle_path),
        "path": jingle_path,
        "after_track": aktif_dosya,
        "active": True,
        "created_at": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
    })
    ayarlar["scheduled_playlist_inserts"] = ekler
    ayarlari_yaz(ayarlar)
    playlist_yaz(ayarlar.get("active_playlist", "all"), ayarlar)
    liquidsoap_komut("music.reload empty_queue=true")
    jingle_status_yaz(
        last_queue="Playlist sirasi",
        last_queue_name=jingle.get("name") or os.path.basename(jingle_path),
        last_queue_success=True,
        last_queue_paths=[jingle_path],
        last_queue_reason="Jingle playlistte mevcut sarkinin arkasina eklendi",
    )
    return True


def playlist_jingle_temizle(ayarlar):
    if not INJECTED_PLAYLIST_JINGLES:
        return
    dosyalar = [path for path in m3u_oku() if path not in INJECTED_PLAYLIST_JINGLES]
    if dosyalar:
        m3u_yaz(dosyalar)
        liquidsoap_komut("music.reload")
    INJECTED_PLAYLIST_JINGLES.clear()
    PLAYED_PLAYLIST_JINGLES.clear()


def gecici_playlist_eklerini_temizle(ayarlar, metadata):
    filename = (metadata.get("filename") or "").replace("\\", "/")
    if not filename:
        return ayarlar
    ekler = ayarlar.get("scheduled_playlist_inserts", [])
    if not ekler:
        return ayarlar
    kalan = [ek for ek in ekler if (ek.get("path") or "").replace("\\", "/") != filename]
    if len(kalan) != len(ekler):
        ayarlar["scheduled_playlist_inserts"] = kalan
        ayarlari_yaz(ayarlar)
    return ayarlar


def liq_string(value):
    return str(value).replace("\\", "\\\\").replace('"', '\\"')


def liq_annotate_value(value):
    return str(value or "").replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ").replace("\r", " ")


def saat_kosulu(jingle):
    minute = int(jingle.get("minute", 0))
    end_minute = (minute + 1) % 60
    hour = jingle.get("hour", "")
    if hour == "":
        return f"{minute}m-{end_minute}m"
    return f"{int(hour)}h{minute}m-{int(hour)}h{end_minute}m"


def ses_presetleri():
    return {
        "natural": {"bass": 0, "low_mid": 0, "presence": 0, "air": 0, "multiband": 0, "compressor": 1, "gain": 0, "normalize": False, "limiter": True},
        "pop_radio": {"bass": 3, "low_mid": -1, "presence": 2, "air": 1.5, "multiband": 2, "compressor": 4, "gain": 1, "normalize": True, "limiter": True},
        "bass_strong": {"bass": 6, "low_mid": -0.5, "presence": 1.5, "air": 1, "multiband": 2, "compressor": 4, "gain": 0, "normalize": True, "limiter": True},
        "bright": {"bass": 2, "low_mid": -1.5, "presence": 3, "air": 2.5, "multiband": 3, "compressor": 5, "gain": 1, "normalize": True, "limiter": True},
        "night_soft": {"bass": 1, "low_mid": -0.5, "presence": 0.5, "air": 0, "multiband": 1, "compressor": 2, "gain": -1, "normalize": True, "limiter": True},
    }


def ses_ayarlarini_hazirla(ayarlar):
    preset = ses_presetleri().get(ayarlar.get("audio_preset", "pop_radio"), ses_presetleri()["pop_radio"])
    def sayi(key, default, minimum, maximum):
        try:
            return max(minimum, min(maximum, float(ayarlar.get(key, default))))
        except (TypeError, ValueError):
            return default

    bass = sayi("bass_gain", preset["bass"], 0, 10)
    low_mid = sayi("low_mid_gain", preset["low_mid"], -6, 6)
    presence = sayi("presence_gain", preset["presence"], -6, 6)
    air = sayi("air_gain", preset["air"], -6, 6)
    multiband = sayi("multiband_strength", preset["multiband"], 0, 10)
    compressor = sayi("compressor_strength", preset["compressor"], 0, 10)
    gain = sayi("output_gain", preset["gain"], -6, 6)
    normalize_on = ayarlar.get("normalize_audio", preset["normalize"])
    limiter_on = ayarlar.get("limiter_enabled", preset["limiter"])
    ratio = 1.0 + (compressor * 0.7)
    threshold = -8.0 - (compressor * 1.1)
    makeup = min(4.0, compressor * 0.35)
    return {
        "bass": bass,
        "low_mid": low_mid,
        "presence": presence,
        "air": air,
        "multiband": multiband,
        "compressor": compressor,
        "gain": gain,
        "normalize": normalize_on,
        "limiter": limiter_on,
        "ratio": ratio,
        "threshold": threshold,
        "makeup": makeup,
    }


def liquidsoap_yaz(ayarlar):
    secili = ayarlar.get("active_playlist", "all")
    dosyalar = muzik_dosyalari(secili)
    if not dosyalar:
        dosyalar = muzik_dosyalari("all")
    m3u_yaz(yayin_akisi_dosyalari(playlist_sirala(dosyalar, ayarlar, secili), ayarlar))
    try:
        jingle_amp = max(0, min(200, int(ayarlar.get("jingle_volume", 100)))) / 100
    except (TypeError, ValueError):
        jingle_amp = 1.0
    try:
        jingle_transition = max(0, min(7, float(ayarlar.get("jingle_transition", 0))))
    except (TypeError, ValueError):
        jingle_transition = 0.0
    try:
        # jingle_end_before varsa onu kullan (jingle bitişinde müzik kaç sn önce girer)
        # yoksa geriye dönük uyumluluk için jingle_music_overlap'ı kullan
        _jeb = ayarlar.get("jingle_end_before")
        _jmo = ayarlar.get("jingle_music_overlap", 1.5)
        jingle_music_overlap = max(0, min(7, float(_jeb if _jeb is not None else _jmo)))
    except (TypeError, ValueError):
        jingle_music_overlap = 2.0
    try:
        jingle_duck = max(0, min(100, int(ayarlar.get("jingle_duck_volume", 35)))) / 100
    except (TypeError, ValueError):
        jingle_duck = 0.35
    try:
        crossfade_duration = max(0, float(ayarlar.get("crossfade_duration", 3)))
        fade_in = max(0, float(ayarlar.get("fade_in", 2)))
        fade_out = max(0, float(ayarlar.get("fade_out", 2)))
    except (TypeError, ValueError):
        crossfade_duration, fade_in, fade_out = 6.0, 1.0, 4.0

    satirlar = [
        'set("log.stdout", true)',
        'set("server.telnet", true)',
        'set("server.telnet.bind_addr", "0.0.0.0")',
        'set("server.telnet.port", 1234)',
        "",
        f'music_raw = playlist(id="music", mode="normal", prefetch=1, reload_mode="watch", "{liq_string(PLAYLIST_YOLU)}")',
        'scheduled_next = request.queue(id="scheduled_next")',
        'music = fallback(track_sensitive=true, [scheduled_next, music_raw])',
    ]

    if ayarlar.get("audio_processing_enabled", False):
        ses = ses_ayarlarini_hazirla(ayarlar)
        if ses["normalize"]:
            satirlar.append('music = normalize(target=-14., gain_min=-8., gain_max=8., threshold=-45., music)')
        if ses["bass"] > 0:
            satirlar.append(f'music = bass_boost(frequency=180., gain={ses["bass"]:.2f}, music)')
        if ses["multiband"] > 0 or ses["low_mid"] != 0 or ses["presence"] != 0 or ses["air"] != 0:
            multiband_ratio = 1.0 + (ses["multiband"] * 0.18)
            satirlar.append(
                "music = compress.multiband(music, ["
                f"{{frequency=180., attack=25., release=140., threshold=-18., ratio={multiband_ratio:.2f}, gain=0.00}}, "
                f"{{frequency=700., attack=25., release=140., threshold=-18., ratio={multiband_ratio:.2f}, gain={ses['low_mid']:.2f}}}, "
                f"{{frequency=3000., attack=20., release=120., threshold=-18., ratio={multiband_ratio:.2f}, gain=0.00}}, "
                f"{{frequency=8000., attack=15., release=100., threshold=-18., ratio={multiband_ratio:.2f}, gain={ses['presence']:.2f}}}, "
                f"{{frequency=20000., attack=10., release=90., threshold=-18., ratio={multiband_ratio:.2f}, gain={ses['air']:.2f}}}"
                "])"
            )
        if ses["compressor"] > 0:
            satirlar.append(
                f'music = compress(attack=20., release=180., threshold={ses["threshold"]:.2f}, ratio={ses["ratio"]:.2f}, gain={ses["makeup"]:.2f}, knee=2., music)'
            )
        if ses["limiter"]:
            satirlar.append('music = compress(attack=5., release=80., threshold=-1.5, ratio=20., gain=0., knee=0.5, music)')
        if ses["gain"] != 0:
            satirlar.append(f'music = amplify({10 ** (ses["gain"] / 20):.4f}, music)')

    if ayarlar.get("normalize_audio", False) and not ayarlar.get("audio_processing_enabled", False):
        satirlar.append("music = normalize(music)")
    if ayarlar.get("trim_silence_enabled", True):
        try:
            trim_seconds = max(1, min(8, float(ayarlar.get("trim_silence_seconds", 3))))
            trim_threshold = min(-10, float(ayarlar.get("trim_silence_threshold", ayarlar.get("silence_threshold", -45))))
        except (TypeError, ValueError):
            trim_seconds, trim_threshold = 3.0, -45.0
        satirlar.extend([
            f"music = blank.eat(at_beginning=true, max_blank={trim_seconds:.2f}, min_noise=0.20, threshold={trim_threshold:.2f}, music)",
            f"music = blank.skip(max_blank={trim_seconds:.2f}, min_noise=0.20, threshold={trim_threshold:.2f}, music)",
        ])
    if ayarlar.get("silence_detection_enabled", True):
        try:
            silence_seconds = max(5, float(ayarlar.get("silence_seconds", 20)))
            silence_threshold = min(-10, float(ayarlar.get("silence_threshold", -45)))
        except (TypeError, ValueError):
            silence_seconds, silence_threshold = 20.0, -45.0
        satirlar.extend([
            "def silence_handler() =",
            '  log("Sessizlik algilandi, sonraki parcaya geciliyor.")',
            "  music_raw.skip()",
            "end",
            f"music = blank.detect(max_blank={silence_seconds:.2f}, threshold={silence_threshold:.2f}, silence_handler, music)",
        ])

    # Mimari:
    #   1) crossfade sadece music uzerinde — sarkilar arasi gecis (jingle etkilenmez)
    #   2) jingle_next crossfade'in DISINDA fallback ile — jingle tam calip biter
    #   3) jingle bitince track_sensitive=true sayesinde muzige geri doner
    if ayarlar.get("crossfade_enabled", True) and crossfade_duration > 0:
        smart = "true" if ayarlar.get("smart_crossfade", False) else "false"
        satirlar.append(
            f"music = crossfade(duration={crossfade_duration:.2f}, fade_in={fade_in:.2f}, fade_out={fade_out:.2f}, smart={smart}, music)"
        )
    satirlar.append("music = mksafe(music)")

    satirlar.extend([
        'jingle_now_raw = request.queue(id="jingle_now")',
        'jingle_next_raw = request.queue(id="jingle_next")',
        'jingle_over_raw = request.queue(id="jingle_over")',
        f"jingle_now = amplify({jingle_amp:.2f}, jingle_now_raw)",
        f"jingle_next = amplify({jingle_amp:.2f}, jingle_next_raw)",
        f"jingle_over = amplify({jingle_amp:.2f}, jingle_over_raw)",
    ])

    if jingle_transition > 0:
        satirlar.extend([
            f"jingle_now = fade.in(duration={jingle_transition:.2f}, jingle_now)",
            f"jingle_now = fade.out(duration={jingle_transition:.2f}, jingle_now)",
            f"jingle_over = fade.in(duration={jingle_transition:.2f}, jingle_over)",
            f"jingle_over = fade.out(duration={jingle_transition:.2f}, jingle_over)",
        ])

    satirlar.append(f"normal_radio = fallback(track_sensitive=true, [jingle_next, music])")
    satirlar.append("normal_radio = mksafe(normal_radio)")

    # jingle_over: şarkının üstüne girer, şarkı p oranında kısılır
    # duration=0.05 → giriş neredeyse anında (fade yok), çıkışta 0.05s blend
    satirlar.extend([
        f"overlay_radio = smooth_add(duration=0.05, p={jingle_duck:.2f}, normal=normal_radio, special=jingle_over)",
    ])

    dj_sources = []
    for slot in dj_slotlari_hazirla(ayarlar):
        try:
            port = int(slot.get("port", 0) or 0)
        except (TypeError, ValueError):
            port = 0
        if port <= 0:
            continue
        source_id = (slot.get("mount") or f"/dj-{port}").strip().lstrip("/")
        source_var = re.sub(r"[^A-Za-z0-9_]", "_", source_id)
        password = liq_string(slot.get("password", ""))
        dj_gain = 1.0
        mount = f"/{liq_string(source_id)}"
        satirlar.extend([
            f'{source_var}_raw = input.harbor(id="{liq_string(source_id)}", "{mount}", port={port}, password="{password}")',
            f'{source_var} = amplify({dj_gain:.2f}, {source_var}_raw)',
        ])
        dj_sources.append(source_var)

    if dj_sources:
        satirlar.append(f"dj_radio = fallback(track_sensitive=false, [{', '.join(dj_sources)}])")
        satirlar.append("radio = fallback(track_sensitive=false, [dj_radio, jingle_now, overlay_radio])")
    else:
        satirlar.append("radio = fallback(track_sensitive=false, [jingle_now, overlay_radio])")
    satirlar.append("radio = mksafe(radio)")

    if ayarlar.get("recording_enabled", False):
        kayit_yolu = "/web-radyo/recordings/%Y-%m-%d/%H-%M.mp3"
        satirlar.append(f'output.file(id="recording", %mp3, "{kayit_yolu}", radio)')

    icecast_args = [
        "%mp3",
        'host="icecast-server"',
        "port=8000",
        'password="hackme"',
        'mount="/radyo"',
        f'name="{liq_string(ayarlar.get("station_name", "Radyo Pop"))}"',
        f'genre="{liq_string(ayarlar.get("station_genre", "Pop"))}"',
    ]
    if ayarlar.get("station_url"):
        icecast_args.append(f'url="{liq_string(ayarlar.get("station_url", ""))}"')
    if ayarlar.get("station_description"):
        icecast_args.append(f'description="{liq_string(ayarlar.get("station_description", ""))}"')
    icecast_args.append("radio")
    satirlar.extend([
        f"output.icecast({', '.join(icecast_args)})",
        "",
    ])

    with open(LIQ_YOLU, "w", encoding="utf-8") as f:
        f.write("\n".join(satirlar))


def playlist_yaz(playlist_key, ayarlar=None):
    ayarlar = ayarlar or ayarlari_oku()
    exclude_playlists = araya_giren_playlist_anahtarlari(ayarlar, playlist_key) if playlist_key == "all" else []
    dosyalar = muzik_dosyalari(playlist_key, exclude_playlists=exclude_playlists)
    if not dosyalar:
        dosyalar = muzik_dosyalari("all", exclude_playlists=exclude_playlists)
    sirali_dosyalar = playlist_sirala(dosyalar, ayarlar, playlist_key)
    akis = yayin_akisi_dosyalari(sirali_dosyalar, ayarlar, playlist_key)
    m3u_yaz(gecici_playlist_eklerini_uygula(akis, ayarlar))


def aktif_playlisti_yenile(ayarlar=None, skip=False):
    ayarlar = ayarlar or ayarlari_oku()
    active_playlist = ayarlar.get("active_playlist", "all")
    playlist_yaz(active_playlist, ayarlar)
    reloaded = liquidsoap_komut("music.reload")
    if skip:
        return reloaded and liquidsoap_komut("music.skip")
    return reloaded


def calan_listeyi_degistir(playlist_key):
    global PLAYLIST_ROUND_PLAYED, LAST_PLAYLIST_ROUND_KEY
    PLAYLIST_ROUND_PLAYED = set()
    LAST_PLAYLIST_ROUND_KEY = playlist_key
    ayarlar = ayarlari_oku()
    ayarlar["active_playlist"] = playlist_key
    return aktif_playlisti_yenile(ayarlar, skip=True)


def playlist_turunu_izle(ayarlar, metadata):
    global PLAYLIST_ROUND_PLAYED, LAST_PLAYLIST_ROUND_KEY
    if not playlist_karisik_mi(ayarlar, ayarlar.get("active_playlist", "all")):
        return
    filename = (metadata.get("filename") or "").replace("\\", "/")
    if not filename.startswith(MUZIK_YOLU.replace("\\", "/")):
        return
    playlist_key = ayarlar.get("active_playlist", "all")
    if LAST_PLAYLIST_ROUND_KEY != playlist_key:
        PLAYLIST_ROUND_PLAYED = set()
        LAST_PLAYLIST_ROUND_KEY = playlist_key
    PLAYLIST_ROUND_PLAYED.add(filename)
    current_list = [path.replace("\\", "/") for path in m3u_oku() if path.replace("\\", "/").startswith(MUZIK_YOLU.replace("\\", "/"))]
    total = len(set(current_list))
    if total > 1 and len(PLAYLIST_ROUND_PLAYED) >= total:
        playlist_yaz(playlist_key, ayarlar)
        liquidsoap_komut("music.reload")
        PLAYLIST_ROUND_PLAYED = {filename}
        jingle_status_yaz(last_playlist_shuffle=dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"))


def saat_araliginda(now_time, start, end):
    start_time = dt.time.fromisoformat(start)
    end_time = dt.time.fromisoformat(end)
    if start_time <= end_time:
        return start_time <= now_time < end_time
    return now_time >= start_time or now_time < end_time


def otomatik_playlist_kontrol(ayarlar, simdi):
    degisti = False
    for rule in ayarlar.get("schedules", []):
        if not rule.get("active", True):
            continue
        start_date = rule.get("start_date", "")
        end_date = rule.get("end_date", "")
        if start_date:
            try:
                if simdi.date() < dt.date.fromisoformat(start_date):
                    continue
            except ValueError:
                pass
        if end_date:
            try:
                if simdi.date() > dt.date.fromisoformat(end_date):
                    continue
            except ValueError:
                pass
        days = [str(day) for day in (rule.get("days") or [])]
        if days and str(simdi.weekday()) not in days:
            continue
        if rule.get("time") != simdi.strftime("%H:%M"):
            continue
        playlist = rule.get("playlist", "")
        anahtar = f'schedule:{simdi:%Y-%m-%d-%H-%M}:{rule.get("id")}'
        if playlist and playlist != ayarlar.get("active_playlist") and anahtar not in LAST_FIRED:
            ayarlar["active_playlist"] = playlist
            ayarlari_yaz(ayarlar)
            calan_listeyi_degistir(playlist)
            LAST_FIRED.add(anahtar)
            if rule.get("once"):
                rule["active"] = False
                degisti = True

    if degisti:
        ayarlari_yaz(ayarlar)

    day_playlist = ayarlar.get("day_playlist", "")
    night_playlist = ayarlar.get("night_playlist", "")
    if not day_playlist and not night_playlist:
        return
    day_start = ayarlar.get("day_start", "07:00")
    night_start = ayarlar.get("night_start", "22:00")
    hedef = day_playlist if saat_araliginda(simdi.time(), day_start, night_start) else night_playlist
    if hedef and hedef != ayarlar.get("active_playlist"):
        ayarlar["active_playlist"] = hedef
        ayarlari_yaz(ayarlar)
        calan_listeyi_degistir(hedef)


def yayini_yenile():
    ayarlar = ayarlari_oku()
    liquidsoap_yaz(ayarlar)
    docker_komut("restart")


def liquidsoap_komut(komut):
    son_cevap = ""
    try:
        with socket.create_connection(("radio-engine", 1234), timeout=2) as baglanti:
            baglanti.sendall((komut + "\n").encode("utf-8"))
            baglanti.settimeout(3)
            parcali = []
            while True:
                try:
                    parca = baglanti.recv(4096)
                except socket.timeout:
                    break
                if not parca:
                    break
                parcali.append(parca.decode("utf-8", errors="ignore"))
                if "END" in parcali[-1]:
                    break
            baglanti.sendall(b"exit\n")
            son_cevap = "".join(parcali).strip()
            if "ERROR" in son_cevap.upper():
                print(f"Liquidsoap komut cevabi hata: {komut} -> {son_cevap}")
                return False
            return "END" in son_cevap or son_cevap.isdigit()
    except OSError as hata:
        print(f"Liquidsoap komut hatasi: {hata}")
        return False
    finally:
        if ".push " in komut and son_cevap:
            print(f"Liquidsoap komut cevabi: {komut} -> {son_cevap}")


def liquidsoap_cevap(komut):
    try:
        with socket.create_connection(("radio-engine", 1234), timeout=2) as baglanti:
            baglanti.sendall((komut + "\n").encode("utf-8"))
            baglanti.settimeout(1)
            parcali = []
            while True:
                try:
                    parca = baglanti.recv(4096)
                except socket.timeout:
                    break
                if not parca:
                    break
                metin = parca.decode("utf-8", errors="ignore")
                parcali.append(metin)
                if "END" in metin:
                    break
            baglanti.sendall(b"exit\n")
            return "".join(parcali)
    except OSError:
        return ""


def jingle_kuyruklarini_temizle():
    liquidsoap_komut("jingle_now.flush_and_skip")
    liquidsoap_komut("jingle_next.flush_and_skip")
    liquidsoap_komut("jingle_over.flush_and_skip")


def bekleyen_jingleleri_temizle(jingle_id=None):
    if jingle_id:
        for anahtar, item in list(PENDING_AFTER_JINGLES.items()):
            jingle = item.get("jingle") or {}
            if jingle.get("id") == jingle_id:
                PENDING_AFTER_JINGLES.pop(anahtar, None)
    else:
        PENDING_AFTER_JINGLES.clear()
    jingle_status_yaz(
        pending_count=len(PENDING_AFTER_JINGLES),
        pending_names=[pending.get("name", "") for pending in PENDING_AFTER_JINGLES.values()],
    )


def jingle_kuyrugu_bos_mu(kuyruk):
    return liquidsoap_cevap(f"{kuyruk}.queue").replace("END", "").strip() == ""


def metadata_alani(metadata, alan):
    prefix = f'{alan}="'
    for satir in metadata.splitlines():
        if satir.startswith(prefix):
            return satir[len(prefix):-1]
    return ""


def saat_anons_playlist_klasoru_hazirla(key):
    hedef_rel = f"{AUTO_HOUR_PARENT}/{key}"
    hedef = guvenli_yol("muzik", hedef_rel)
    eski = guvenli_yol("muzik", key)
    os.makedirs(hedef, exist_ok=True)
    if os.path.isdir(eski) and os.path.abspath(eski) != os.path.abspath(hedef):
        for name in os.listdir(eski):
            kaynak = os.path.join(eski, name)
            hedef_item = os.path.join(hedef, name)
            if not os.path.exists(hedef_item):
                shutil.move(kaynak, hedef_item)
        try:
            os.rmdir(eski)
        except OSError:
            pass
    return hedef_rel


def metadata_sozluk(metadata):
    return {
        "rid": metadata_alani(metadata, "rid"),
        "source": metadata_alani(metadata, "source"),
        "title": metadata_alani(metadata, "title"),
        "artist": metadata_alani(metadata, "artist"),
        "filename": metadata_alani(metadata, "filename") or metadata_alani(metadata, "initial_uri"),
        "jingle_id": metadata_alani(metadata, "jingle_id"),
        "history_enabled": metadata_alani(metadata, "history_enabled"),
        "on_air": metadata_alani(metadata, "on_air"),
        "on_air_timestamp": metadata_alani(metadata, "on_air_timestamp"),
        "status": metadata_alani(metadata, "status"),
    }


def track_elapsed_seconds(metadata):
    raw = metadata.get("on_air_timestamp")
    try:
        if raw:
            return max(0, int(time.time() - float(raw)))
    except (TypeError, ValueError):
        pass
    raw_date = metadata.get("on_air")
    if raw_date:
        try:
            started = dt.datetime.strptime(raw_date, "%Y/%m/%d %H:%M:%S").replace(tzinfo=ISTANBUL_TZ)
            return max(0, int((dt.datetime.now(ISTANBUL_TZ) - started).total_seconds()))
        except ValueError:
            return 0
    return 0


def track_duration_seconds(filename):
    if not filename:
        return 0
    cache = duration_cache_oku()
    try:
        stat = os.stat(filename)
    except OSError:
        return 0
    marker = f"{int(stat.st_mtime)}:{stat.st_size}"
    item = cache.get(filename, {})
    if item.get("marker") != marker:
        item = {"marker": marker, "duration": dosya_suresi_saniye(filename)}
        cache[filename] = item
        duration_cache_yaz(cache)
    return int(item.get("duration", 0))


def track_duration_recalculate(filename):
    if not filename:
        return 0
    try:
        stat = os.stat(filename)
    except OSError:
        return 0
    duration = dosya_suresi_saniye(filename)
    cache = duration_cache_oku()
    cache[filename] = {"marker": f"{int(stat.st_mtime)}:{stat.st_size}", "duration": duration}
    duration_cache_yaz(cache)
    return int(duration or 0)


def metadata_zamanlari(metadata):
    elapsed = track_elapsed_seconds(metadata)
    filename = metadata.get("filename", "")
    duration = track_duration_seconds(filename)
    if duration and elapsed > duration + 5:
        duration = max(duration, track_duration_recalculate(filename))
    remaining_from_engine = 0
    try:
        # Liquidsoap komut adı: istasyon adındaki boşluklar _ ile değiştirilir
        # Örn: "Radyo Pop" → "Radyo_Pop.remaining"
        # Station cmd'yi cache'den al, yoksa ayarlardan oku
        if _LIQ_STATION_CMD[0] is None:
            _station_raw = ayarlari_oku().get("station_name", "Radyo_Pop")
            _LIQ_STATION_CMD[0] = re.sub(r"[^A-Za-z0-9_]", "_", str(_station_raw))
        remaining_response = liquidsoap_cevap(f"{_LIQ_STATION_CMD[0]}.remaining").replace("END", "").strip()
        if remaining_response and not remaining_response.startswith("ERROR"):
            remaining_from_engine = int(float(remaining_response.split()[0]))
        else:
            _LIQ_STATION_CMD[0] = None  # komut çalışmadı, bir sonraki seferde tekrar dene
    except (TypeError, ValueError, IndexError):
        remaining_from_engine = 0
    if remaining_from_engine > 0:
        remaining = remaining_from_engine
        duration = max(duration, elapsed + remaining)
    else:
        remaining = max(0, duration - elapsed) if duration else 0
    if duration and elapsed > duration + 5:
        duration = 0
        remaining = 0
    metadata["elapsed_seconds"] = elapsed
    metadata["duration_seconds"] = duration
    metadata["remaining_seconds"] = remaining
    metadata["elapsed"] = sure_formatla(elapsed)
    metadata["duration"] = sure_formatla(duration) if duration else ""
    metadata["remaining"] = sure_formatla(remaining) if duration else ""
    metadata["progress"] = min(100, int((elapsed / duration) * 100)) if duration else 0
    return metadata


def metadata_gorunumunu_duzelt(metadata):
    filename = metadata.get("filename") or ""
    if filename:
        fallback_artist, fallback_title = dosya_adindan_meta(filename)
        if not metadata.get("title") or metadata.get("title") == filename:
            metadata["title"] = fallback_title
        if not metadata.get("artist") and fallback_artist:
            metadata["artist"] = fallback_artist
    return metadata


def aktif_muzik_rid():
    metadata = aktif_metadata()
    return muzik_rid_from_metadata(metadata)


def muzik_rid_from_metadata(metadata):
    filename = (metadata.get("filename") or "").replace("\\", "/")
    source = metadata.get("source")
    # Liquidsoap bazi filtrelerden sonra source alanini bos/degisik dondurebiliyor.
    # Dosya yolu muzik klasorundeyse bunu gercek parca kabul edip jingle sayacini ilerletiyoruz.
    if source != "music" and not filename.startswith(MUZIK_YOLU.replace("\\", "/")):
        return None
    if filename.startswith(JINGLE_YOLU.replace("\\", "/")):
        return None
    rid = metadata.get("rid")
    return f"{rid or 'track'}:{filename}"


def metadata_jingle_mi(metadata):
    filename = (metadata.get("filename") or "").replace("\\", "/")
    source = metadata.get("source") or ""
    return filename.startswith(JINGLE_YOLU.replace("\\", "/")) or source.startswith("jingle")


def aktif_metadata():
    on_air = liquidsoap_cevap("request.on_air")
    ridler = []
    for token in on_air.replace("END", "").split():
        if token.isdigit():
            ridler.append(token)
    adaylar = []
    for rid in ridler:
        metadata = metadata_sozluk(liquidsoap_cevap(f"request.metadata {rid}"))
        if metadata.get("rid"):
            adaylar.append(metadata)
    calanlar = [metadata for metadata in adaylar if metadata.get("status") == "playing"]
    for metadata in calanlar:
        if metadata_jingle_mi(metadata):
            return metadata_gorunumunu_duzelt(metadata_zamanlari(metadata))
    if calanlar:
        return metadata_gorunumunu_duzelt(metadata_zamanlari(calanlar[0]))
    if adaylar:
        return metadata_gorunumunu_duzelt(metadata_zamanlari(adaylar[0]))
    return metadata_gorunumunu_duzelt(metadata_zamanlari(metadata_sozluk(liquidsoap_cevap("/radyo.metadata"))))


def gecmis_oku():
    if not os.path.exists(HISTORY_YOLU):
        return []
    try:
        with open(HISTORY_YOLU, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return []


def gecmis_yaz(gecmis):
    os.makedirs(STATE_YOLU, exist_ok=True)
    with open(HISTORY_YOLU, "w", encoding="utf-8") as f:
        json.dump(gecmis[:PLAY_HISTORY_LIMIT], f, ensure_ascii=False, indent=2)


def jingle_gecmis_kaydi_mi(kayit):
    filename = (kayit.get("filename") or "").replace("\\", "/")
    return filename.startswith(JINGLE_YOLU.replace("\\", "/")) or str(kayit.get("source", "")).startswith("jingle")


def gecmise_ekle(metadata):
    if not metadata.get("filename"):
        return
    filename = metadata.get("filename") or ""
    source = metadata.get("source") or ""
    is_jingle = filename.replace("\\", "/").startswith(JINGLE_YOLU.replace("\\", "/"))
    if is_jingle:
        if str(metadata.get("history_enabled", "true")).lower() == "false":
            return
        title = metadata.get("title") or os.path.basename(filename)
        artist = metadata.get("artist") or ayarlari_oku().get("station_name", "")
    else:
        title = metadata.get("title") or os.path.basename(filename)
        artist = metadata.get("artist") or ""
    kayit = {
        "time": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
        "title": title,
        "artist": artist,
        "filename": filename,
        "source": source,
    }
    gecmis = gecmis_oku()
    if gecmis and gecmis[0].get("filename") == kayit["filename"] and gecmis[0].get("source") == kayit["source"]:
        return
    for eski in gecmis[:5]:
        if eski.get("filename") != kayit["filename"]:
            continue
        try:
            eski_zaman = dt.datetime.strptime(eski.get("time", ""), "%d.%m.%Y %H:%M:%S").replace(tzinfo=ISTANBUL_TZ)
        except (TypeError, ValueError):
            continue
        if (dt.datetime.now(ISTANBUL_TZ) - eski_zaman).total_seconds() < 30:
            return
    if not is_jingle:
        for eski in gecmis[:10]:
            if jingle_gecmis_kaydi_mi(eski):
                continue
            if eski.get("filename") == kayit["filename"] and eski.get("source") == kayit["source"]:
                return
            break
    gecmis.insert(0, kayit)
    gecmis_yaz(gecmis)


def jingle_zamaninda_mi(jingle, simdi):
    if not jingle.get("active", True):
        return False
    days = [str(day) for day in (jingle.get("days") or [])]
    if days and str(simdi.weekday()) not in days:
        return False
    start = jingle.get("start_time", "")
    end = jingle.get("end_time", "")
    if start and end:
        now_time = simdi.time()
        start_time = dt.time.fromisoformat(start)
        end_time = dt.time.fromisoformat(end)
        if start_time <= end_time:
            return start_time <= now_time <= end_time
        return now_time >= start_time or now_time <= end_time
    return True


def jingle_dosya_yollari(jingle):
    paths = [path for path in jingle.get("paths", []) if path]
    if not paths and jingle.get("path"):
        paths = [jingle.get("path")]
    return paths


def jingle_gosterim_metasi(jingle, path):
    title = (jingle.get("custom_title") or "").strip()
    artist = (jingle.get("custom_artist") or "").strip()
    if not title:
        title = (jingle.get("name") or os.path.splitext(os.path.basename(path))[0]).strip()
    if not artist:
        artist = ayarlari_oku().get("station_name", "Radyo")
    return artist, title


def jingle_annotated_path(jingle, path):
    artist, title = jingle_gosterim_metasi(jingle, path)
    history_enabled = "true" if jingle.get("save_history", True) else "false"
    jingle_id = jingle.get("id") or ""
    annotations = [
        f'artist="{liq_annotate_value(artist)}"',
        f'title="{liq_annotate_value(title)}"',
        f'jingle_id="{liq_annotate_value(jingle_id)}"',
        f'history_enabled="{history_enabled}"',
    ]
    return "annotate:" + ",".join(annotations) + ":" + path


def playlist_annotated_path(playlist, path):
    artist, title = dosya_adindan_meta(path)
    playlist_name = (playlist.get("name") or title or os.path.splitext(os.path.basename(path))[0]).strip()
    annotations = [
        f'title="{liq_annotate_value(playlist_name)}"',
        f'playlist_id="{liq_annotate_value(playlist.get("id") or "")}"',
        'history_enabled="true"',
    ]
    if artist:
        annotations.append(f'artist="{liq_annotate_value(artist)}"')
    return "annotate:" + ",".join(annotations) + ":" + path


def jingle_kuyruk_yollari(jingle):
    paths = jingle_dosya_yollari(jingle)
    if len(paths) <= 1 or not jingle.get("rotate"):
        return paths

    rotation_key = jingle.get("id") or jingle.get("name") or "|".join(paths)
    index = JINGLE_ROTATION_INDEX.get(rotation_key, 0) % len(paths)
    JINGLE_ROTATION_INDEX[rotation_key] = (index + 1) % len(paths)
    return [paths[index]]


def jingle_kuyruga_at(jingle):
    paths = jingle_kuyruk_yollari(jingle)
    if not paths:
        return False
    kuyruk = jingle.get("force_queue") or ("jingle_now" if jingle.get("cut_policy") == "cut" else ("jingle_over" if jingle.get("cut_policy") == "over" else "jingle_next"))
    flush_first = bool(jingle.get("flush_first"))
    if flush_first:
        liquidsoap_komut(f"{kuyruk}.flush_and_skip")
    if kuyruk in ("jingle_next", "jingle_over") and jingle.get("allow_stack") is not True and not flush_first:
        queue = liquidsoap_cevap(f"{kuyruk}.queue").replace("END", "").strip()
        if queue:
            jingle_status_yaz(
                last_queue="Atlandi",
                last_queue_name=jingle.get("name") or os.path.basename(paths[0]),
                last_queue_success=False,
                last_queue_reason=f"{kuyruk} kuyrugunda bekleyen jingle var",
            )
            return False
    basarili = True
    for path in paths:
        komut = f"{kuyruk}.push {jingle_annotated_path(jingle, path)}"
        sonuc = liquidsoap_komut(komut)
        if not sonuc:
            time.sleep(0.5)
            sonuc = liquidsoap_komut(komut)
        basarili = sonuc and basarili
    jingle_status_yaz(
        last_queue=("Simdi" if kuyruk == "jingle_now" else ("Sarkinin uzerine" if kuyruk == "jingle_over" else "Sarki bitince")),
        last_queue_name=jingle.get("name") or os.path.basename(paths[0]),
        last_queue_success=basarili,
        last_queue_paths=paths,
        last_queue_reason="" if basarili else f"{kuyruk} push komutu basarisiz",
    )
    return basarili


def playlist_zaman_penceresinde_mi(playlist, simdi):
    if not playlist.get("active", True):
        return False
    schedule = playlist.get("schedule", {}) or {}
    days = [str(day) for day in (schedule.get("days") or [])]
    if days and str(simdi.weekday()) not in days:
        return False

    today = simdi.date()
    try:
        if schedule.get("start_date") and today < dt.date.fromisoformat(schedule["start_date"]):
            return False
        if schedule.get("end_date") and today > dt.date.fromisoformat(schedule["end_date"]):
            return False
    except ValueError:
        pass

    start = schedule.get("start_time", "")
    end = schedule.get("end_time", "")
    if start and end:
        try:
            now_time = simdi.time()
            start_time = dt.time.fromisoformat(start)
            end_time = dt.time.fromisoformat(end)
            if start_time <= end_time:
                return start_time <= now_time <= end_time
            return now_time >= start_time or now_time <= end_time
        except ValueError:
            return True
    if start:
        try:
            return simdi.time() >= dt.time.fromisoformat(start)
        except ValueError:
            return True
    return True


def playlistten_tek_dosya_sec(playlist_key, playlist, ayarlar):
    dosyalar = playlist_sirala(muzik_dosyalari(playlist_key), ayarlar, playlist_key)
    if not dosyalar:
        return ""
    if playlist.get("playback_order") == "random":
        return random.choice(dosyalar)
    rotation_key = playlist.get("id") or playlist_key
    index = PLAYLIST_ROTATION_INDEX.get(rotation_key, 0) % len(dosyalar)
    PLAYLIST_ROTATION_INDEX[rotation_key] = (index + 1) % len(dosyalar)
    return dosyalar[index]


def playlist_kuyruga_at(playlist_key, playlist, ayarlar):
    path = playlistten_tek_dosya_sec(playlist_key, playlist, ayarlar)
    if not path:
        return False
    kes = bool(playlist.get("interrupt_other"))
    planli_jingle = {
        "id": playlist.get("id") or playlist_key,
        "name": playlist.get("name") or playlist_key,
        "paths": [path],
        "cut_policy": "cut" if kes else "after",
        "force_queue": "jingle_now" if kes else "jingle_next",
        "allow_stack": True,
        "rotate": False,
    }
    if not kes:
        sonuc = liquidsoap_komut(f"scheduled_next.push {playlist_annotated_path(planli_jingle, path)}")
        if not sonuc:
            time.sleep(0.5)
            sonuc = liquidsoap_komut(f"scheduled_next.push {playlist_annotated_path(planli_jingle, path)}")
        jingle_status_yaz(
            last_queue="Playlist sirasi",
            last_queue_name=playlist.get("name") or playlist_key,
            last_queue_success=bool(sonuc),
            last_queue_paths=[path],
            last_queue_reason="" if sonuc else "scheduled_next push komutu basarisiz",
        )
        return bool(sonuc)
    return jingle_kuyruga_at(planli_jingle)


def saat_anonsu_bu_dakikada_var_mi(ayarlar, simdi):
    for key, playlist in ayarlar.get("playlists", {}).items():
        if not playlist.get("active", True):
            continue
        if playlist.get("source", "songs") != "songs":
            continue
        if playlist.get("playback_type") != "once_per_hour":
            continue
        if not saat_anonsu_playlisti_mi({"key": key, **playlist}):
            continue
        if not playlist_zaman_penceresinde_mi(playlist, simdi):
            continue
        try:
            minute = int(playlist.get("interval_value", 0))
        except (TypeError, ValueError):
            minute = 0
        schedule = playlist.get("schedule", {}) or {}
        if schedule.get("start_time"):
            try:
                minute = dt.time.fromisoformat(schedule["start_time"]).minute
            except ValueError:
                pass
        if simdi.minute == max(0, min(59, minute)):
            return True
    return False


def bekleyen_playlist_jinglelerini_cal(ayarlar, metadata, aktif_rid, simdi):
    if not PENDING_PLAYLIST_JINGLES or not aktif_rid:
        return
    try:
        crossfade_duration = max(0.1, min(1.0, float(ayarlar.get("crossfade_duration", 0.6))))
    except (TypeError, ValueError):
        crossfade_duration = 0.6
    threshold = max(0.6, crossfade_duration)
    remaining = float(metadata.get("remaining_seconds") or 0)
    if remaining <= 0 or remaining > threshold:
        return
    for pending_key, pending in list(PENDING_PLAYLIST_JINGLES.items()):
        if pending.get("track_rid") and pending.get("track_rid") != aktif_rid:
            PENDING_PLAYLIST_JINGLES.pop(pending_key, None)
            continue
        planli_jingle = dict(pending.get("jingle") or {})
        planli_jingle["force_queue"] = "jingle_now"
        planli_jingle["cut_policy"] = "cut"
        if jingle_kuyruga_at(planli_jingle):
            PENDING_PLAYLIST_JINGLES.pop(pending_key, None)


def playlist_zamanlayici_kontrol(ayarlar, simdi):
    degisti = False
    for key, playlist in ayarlar.get("playlists", {}).items():
        playback_type = playlist.get("playback_type", "general")
        if playback_type not in {"once_per_x_minutes", "once_per_hour"}:
            continue
        if playlist.get("source", "songs") != "songs" or not playlist_zaman_penceresinde_mi(playlist, simdi):
            continue
        try:
            interval_value = int(playlist.get("interval_value", 1))
        except (TypeError, ValueError):
            interval_value = 1

        if playback_type == "once_per_x_minutes":
            interval = max(1, interval_value)
            if simdi.minute % interval != 0:
                continue
            fire_key = f"playlist-min:{simdi:%Y-%m-%d-%H-%M}:{key}"
            if fire_key in LAST_FIRED:
                continue
            if saat_anonsu_bu_dakikada_var_mi(ayarlar, simdi):
                playlist_skip_kaydi_ekle(key, playlist, simdi, "Saat anonsu öncelikli")
                LAST_FIRED.add(fire_key)
                continue
        else:
            schedule = playlist.get("schedule", {}) or {}
            schedule_times = [str(item) for item in (schedule.get("times") or []) if re.match(r"^\d{2}:\d{2}$", str(item))]
            if schedule_times:
                current_time = f"{simdi.hour:02d}:{simdi.minute:02d}"
                if current_time not in schedule_times:
                    continue
                fire_key = f"playlist-time:{simdi:%Y-%m-%d-%H-%M}:{key}"
            else:
                minute = interval_value
                if schedule.get("start_time"):
                    try:
                        minute = dt.time.fromisoformat(schedule["start_time"]).minute
                    except ValueError:
                        minute = interval_value
                minute = max(0, min(59, minute))
                if simdi.minute != minute:
                    continue
                fire_key = f"playlist-hour:{simdi:%Y-%m-%d-%H}:{minute:02d}:{key}"

        if fire_key in LAST_FIRED:
            continue
        if playlist_kuyruga_at(key, playlist, ayarlar):
            LAST_FIRED.add(fire_key)
            if (playlist.get("schedule") or {}).get("once"):
                playlist["active"] = False
                degisti = True

    if degisti:
        ayarlari_yaz(ayarlar)


def siradaki_sarki_icin_jingle_hazirla(jingle, aktif_rid, simdi):
    """Şarkı sayacını günceller.
    over/cut modundaki jingle'lar sayaç dolunca hemen kuyruğa atılır.
    after modundakiler ise sarki_sonu_gecis_tetikle() ile şarkı sonunu bekler."""
    jingle_id = jingle.get("id") or jingle.get("name") or jingle.get("path")
    if not jingle_id or LAST_SONG_JINGLE_TRACK.get(jingle_id) == aktif_rid:
        return False
    if LAST_SONG_COUNTER_TRACK.get(jingle_id) == aktif_rid:
        return False
    try:
        every = max(1, int(jingle.get("every_songs", 3)))
    except (TypeError, ValueError):
        every = 3
    LAST_SONG_COUNTER_TRACK[jingle_id] = aktif_rid
    SONG_COUNTERS[jingle_id] = SONG_COUNTERS.get(jingle_id, 0) + 1
    if SONG_COUNTERS[jingle_id] < every:
        return False
    SONG_COUNTERS[jingle_id] = 0
    LAST_SONG_JINGLE_TRACK[jingle_id] = aktif_rid

    # Sayac doldu — tum modlar icin hemen kuyruga at
    # over → jingle_over (sarki uzerinden), cut → jingle_now (hemen kes), after → jingle_next (sarki bitince)
    BEFORE_END_FIRED[jingle_id] = aktif_rid  # tekrar tetiklenmesini engelle
    basarili = jingle_kuyruga_at(jingle)
    cut_policy = jingle.get("cut_policy", "after")
    jingle_status_yaz(
        last_song_rule=jingle.get("name", ""),
        last_song_rule_at=simdi.strftime("%d.%m.%Y %H:%M:%S"),
        last_song_rule_track=aktif_rid,
        last_queue_reason=f"Sayac doldu, {cut_policy} modunda kuyruga atildi",
    )
    return basarili


def sarki_sonu_gecis_tetikle(jingle, aktif_rid, simdi, remaining, ayarlar):
    """
    remaining <= song_end_before oldugunda jingle'i kuyruga atar.
    Bir sarki icin yalnizca bir kez tetiklenir.
    """
    jingle_id = jingle.get("id") or jingle.get("name") or jingle.get("path")
    if not jingle_id:
        return False
    # Bu sarki icin zaten tetiklendiyse tekrar gonderme
    if BEFORE_END_FIRED.get(jingle_id) == aktif_rid:
        return False
    # Bu jingle bu sarki icin hazir degilse (sayac dolmadi) gonderme
    if LAST_SONG_JINGLE_TRACK.get(jingle_id) != aktif_rid:
        return False

    try:
        song_end_before = max(0.0, float(ayarlar.get("song_end_before", 2.0)))
    except (TypeError, ValueError):
        song_end_before = 2.0

    # remaining bilgisi yoksa (canli yayin vs.) hemen gonder
    if remaining > 0 and remaining > song_end_before:
        return False  # henuz zamani gelmedi

    cut_policy = jingle.get("cut_policy", "after")
    planli_jingle = dict(jingle)
    if cut_policy == "cut":
        planli_jingle["force_queue"] = "jingle_now"
    elif cut_policy == "over":
        planli_jingle["force_queue"] = "jingle_over"
    else:
        planli_jingle["force_queue"] = "jingle_next"
    planli_jingle["flush_first"] = False
    planli_jingle["allow_stack"] = True

    basarili = jingle_kuyruga_at(planli_jingle)
    if basarili:
        BEFORE_END_FIRED[jingle_id] = aktif_rid
        jingle_status_yaz(
            last_song_rule=jingle.get("name", ""),
            last_song_rule_at=simdi.strftime("%d.%m.%Y %H:%M:%S"),
            last_song_rule_track=aktif_rid,
            last_queue_reason=f"Sarki sonu gecisi tetiklendi (remaining={remaining}s, esik={song_end_before}s)",
        )
    return basarili


def planlanmis_sarki_jinglelerini_tekrar_ekle(ayarlar, aktif_rid, onceki_jingle_id=None):
    if not aktif_rid:
        return
    for sarki_jingle in ayarlar.get("jingles", []):
        if not sarki_jingle.get("active", True):
            continue
        if sarki_jingle.get("mode") != "songs" or sarki_jingle.get("cut_policy") != "after":
            continue
        sarki_jingle_id = sarki_jingle.get("id") or sarki_jingle.get("name") or sarki_jingle.get("path")
        if not sarki_jingle_id or sarki_jingle_id == onceki_jingle_id:
            continue
        if LAST_SONG_JINGLE_TRACK.get(sarki_jingle_id) != aktif_rid:
            continue
        planli_jingle = dict(sarki_jingle)
        planli_jingle["force_queue"] = "jingle_next"
        planli_jingle["flush_first"] = False
        planli_jingle["allow_stack"] = True
        jingle_kuyruga_at(planli_jingle)


def sarki_sonu_kuyruk_esigi(ayarlar):
    return 0


def sarki_sonuna_jingle_beklet(anahtar, jingle, aktif_rid, simdi):
    if not aktif_rid:
        return False
    PENDING_AFTER_JINGLES[anahtar] = {
        "jingle": dict(jingle),
        "target_rid": aktif_rid,
        "name": jingle.get("name", ""),
        "created_at": simdi.strftime("%d.%m.%Y %H:%M:%S"),
    }
    jingle_status_yaz(
        last_queue="Sarki bitisi bekliyor",
        last_queue_name=jingle.get("name", ""),
        last_queue_success=True,
        last_after_track=aktif_rid,
        last_queue_reason="Jingle mevcut sarkinin sonuna beklemeye alindi",
        pending_count=len(PENDING_AFTER_JINGLES),
        pending_names=[item.get("name", "") for item in PENDING_AFTER_JINGLES.values()],
    )
    return True


def ayardaki_jingle_aktif_mi(ayarlar, jingle):
    jingle_id = jingle.get("id")
    if not jingle_id:
        return jingle.get("active", True)
    for kayit in ayarlar.get("jingles", []):
        if kayit.get("id") == jingle_id:
            return kayit.get("active", True)
    return False


def bekleyen_son_jingleleri_kuyruga_al(ayarlar, metadata, aktif_rid, simdi):
    """PENDING_AFTER_JINGLES'daki jingle'lari kontrol eder.
    Hedef sarki artik calmiyorsa (rid degisti) jingle'i kuyruga atar."""
    if not PENDING_AFTER_JINGLES:
        return
    for anahtar, item in list(PENDING_AFTER_JINGLES.items()):
        jingle = item.get("jingle")
        if not jingle:
            PENDING_AFTER_JINGLES.pop(anahtar, None)
            continue
        # Jingle hala aktif mi kontrol et
        if not ayardaki_jingle_aktif_mi(ayarlar, jingle):
            PENDING_AFTER_JINGLES.pop(anahtar, None)
            continue
        target_rid = item.get("target_rid")
        # Hedef sarki hala caliyorsa bekle
        if target_rid and aktif_rid == target_rid:
            continue
        # Hedef sarki bitti veya degisti — jingle'i kuyruga at
        PENDING_AFTER_JINGLES.pop(anahtar, None)
        sarki_bitisi_jingle_cal(
            jingle, target_rid or "", simdi,
            sebep="Bekleyen jingle: sarki degisti, kuyruga alindi",
            skip_music=False,
        )
    jingle_status_yaz(
        pending_count=len(PENDING_AFTER_JINGLES),
        pending_names=[j.get("name", "") for j in PENDING_AFTER_JINGLES.values()],
    )


def biten_sarkinin_jinglelerini_cal(ayarlar, biten_rid, simdi):
    """Sarki degistiginde, biten sarkinin rid'ine bagli bekleyen jingle'lari calar."""
    if not biten_rid or not PENDING_AFTER_JINGLES:
        return
    for anahtar, item in list(PENDING_AFTER_JINGLES.items()):
        if item.get("target_rid") != biten_rid:
            continue
        jingle = item.get("jingle")
        if not jingle:
            PENDING_AFTER_JINGLES.pop(anahtar, None)
            continue
        if not ayardaki_jingle_aktif_mi(ayarlar, jingle):
            PENDING_AFTER_JINGLES.pop(anahtar, None)
            continue
        PENDING_AFTER_JINGLES.pop(anahtar, None)
        sarki_bitisi_jingle_cal(
            jingle, biten_rid, simdi,
            sebep="Sarki bitisi: bekleyen jingle kuyruga alindi",
            skip_music=False,
        )


def geciken_son_jingleleri_aktar(biten_rid, yeni_rid):
    """Biten sarkiya bagli pending jingle'lar varsa bunlari yeni sarkiya aktar.
    Boylece bir sonraki sarki bittiginde tetiklenirler."""
    if not biten_rid or not yeni_rid or not PENDING_AFTER_JINGLES:
        return
    for anahtar, item in list(PENDING_AFTER_JINGLES.items()):
        if item.get("target_rid") == biten_rid:
            item["target_rid"] = yeni_rid


def sarki_bitisi_jingle_cal(jingle, biten_rid, simdi, sebep="Sarki bitisi yakalandi", skip_music=True):
    planli_jingle = dict(jingle)
    planli_jingle["force_queue"] = "jingle_now" if skip_music else "jingle_next"
    planli_jingle["flush_first"] = True
    planli_jingle["allow_stack"] = True
    basarili = jingle_kuyruga_at(planli_jingle)
    if basarili and skip_music:
        liquidsoap_komut("music.skip")
    jingle_status_yaz(
        last_after_fire=planli_jingle.get("name", ""),
        last_after_track=biten_rid,
        last_queue_reason=sebep if basarili else "Sarki bitisi jingle'i calinamadi",
        pending_count=len(PENDING_AFTER_JINGLES),
        pending_names=[item.get("name", "") for item in PENDING_AFTER_JINGLES.values()],
    )
    return basarili


def saat_anonsu_bul(ayarlar, simdi):
    folder = ayarlar.get("hourly_announcement_folder", "")
    if not folder:
        return ""
    kok = guvenli_yol("jingle", folder)
    if not os.path.exists(kok):
        return ""
    saat = f"{simdi.hour:02d}"
    for root, _, files in os.walk(kok):
        for file_name in sorted(files, key=str.lower):
            temiz = os.path.splitext(file_name)[0].lower()
            if ses_dosyasi_mi(file_name) and (temiz == saat or temiz.startswith(saat + "-") or temiz.startswith(saat + "_")):
                return rel_to_container_path(root, file_name)
    return ""


def reklam_bloklarini_kontrol(ayarlar, simdi):
    for block in ayarlar.get("ad_blocks", []):
        if not block.get("active", True):
            continue
        days = [str(day) for day in (block.get("days") or [])]
        if days and str(simdi.weekday()) not in days:
            continue
        minute = str(block.get("minute", "0"))
        hour = str(block.get("hour", ""))
        if hour not in ("", "None") and int(hour) != simdi.hour:
            continue
        if int(minute) != simdi.minute:
            continue
        anahtar = f'adblock:{simdi:%Y-%m-%d-%H-%M}:{block.get("id")}'
        if anahtar not in LAST_FIRED:
            jingle_kuyruga_at({
                "name": block.get("name", "Reklam bloğu"),
                "paths": block.get("paths", []),
                "cut_policy": block.get("cut_policy", "after"),
            })
            LAST_FIRED.add(anahtar)


def jingle_scheduler():
    global LAST_TRACK_RID, LAST_MUSIC_FINISH_RID
    while True:
        try:
            ayarlar = ayarlari_oku()
            simdi = dt.datetime.now(ISTANBUL_TZ)
            otomatik_jingle_aktif = not dj_gecis_modu_aktif_mi(ayarlar)
            otomatik_playlist_kontrol(ayarlar, simdi)
            playlist_zamanlayici_kontrol(ayarlar, simdi)
            if otomatik_jingle_aktif:
                reklam_bloklarini_kontrol(ayarlar, simdi)
            metadata = aktif_metadata()
            if metadata.get("filename"):
                gecmise_ekle(metadata)
                ayarlar = gecici_playlist_eklerini_temizle(ayarlar, metadata)
            if metadata_jingle_mi(metadata) and metadata.get("filename") in INJECTED_PLAYLIST_JINGLES:
                PLAYED_PLAYLIST_JINGLES.add(metadata.get("filename"))
                jingle_status_yaz(
                    last_after_fire=os.path.basename(metadata.get("filename", "")),
                    last_queue_reason="Playlist jingle yayina girdi",
                )
            aktif_rid = muzik_rid_from_metadata(metadata)
            jingle_status_yaz(
                scheduler_alive=True,
                current_minute=simdi.strftime("%H:%M"),
                current_music=aktif_rid or "",
                pending_count=len(PENDING_AFTER_JINGLES) + len(PENDING_PLAYLIST_JINGLES),
                pending_names=[j.get("name", "") for j in PENDING_AFTER_JINGLES.values()] + [(p.get("jingle") or {}).get("name", "") for p in PENDING_PLAYLIST_JINGLES.values()],
            )
            yeni_sarki = aktif_rid and aktif_rid != LAST_TRACK_RID
            if yeni_sarki:
                biten_rid = LAST_TRACK_RID
                LAST_TRACK_RID = aktif_rid
                playlist_turunu_izle(ayarlar, metadata)
                if PLAYED_PLAYLIST_JINGLES:
                    playlist_jingle_temizle(ayarlar)
                biten_sarkinin_jinglelerini_cal(ayarlar, biten_rid, simdi)
            else:
                biten_rid = None

            if SCHEDULER_PAUSED_UNTIL and simdi < SCHEDULER_PAUSED_UNTIL:
                time.sleep(1)
                continue

            if not otomatik_jingle_aktif:
                PENDING_AFTER_JINGLES.clear()
                time.sleep(1)
                continue

            bekleyen_playlist_jinglelerini_cal(ayarlar, metadata, aktif_rid, simdi)
            bekleyen_son_jingleleri_kuyruga_al(ayarlar, metadata, aktif_rid, simdi)

            for jingle in ayarlar.get("jingles", []):
                if not jingle_zamaninda_mi(jingle, simdi):
                    continue
                if jingle.get("mode") in ("time", "block"):
                    hour = jingle.get("hour", "")
                    minute = int(jingle.get("minute", 0))
                    if hour not in ("", None) and int(hour) != simdi.hour:
                        continue
                    if minute != simdi.minute:
                        continue
                    anahtar = f'{simdi:%Y-%m-%d-%H-%M}:{jingle.get("id")}'
                    if anahtar not in LAST_FIRED:
                        planli_jingle = dict(jingle)
                        if planli_jingle.get("cut_policy") == "after":
                            planli_jingle["force_queue"] = "jingle_next"
                            planli_jingle["flush_first"] = False
                            planli_jingle["allow_stack"] = True
                        basarili = jingle_kuyruga_at(planli_jingle)
                        if basarili:
                            jingle_status_yaz(last_time_match=jingle.get("name", ""), last_time_match_at=simdi.strftime("%d.%m.%Y %H:%M:%S"))
                            LAST_FIRED.add(anahtar)
                elif jingle.get("mode") == "songs" and aktif_rid:
                    elapsed = int(metadata.get("elapsed_seconds") or 0)
                    remaining = float(metadata.get("remaining_seconds") or 0)
                    # Sayaç: şarkı yeteri kadar oynadıysa ve henüz tetiklenmediyse kaydet
                    if elapsed >= 15 and (remaining == 0 or remaining >= 10):
                        siradaki_sarki_icin_jingle_hazirla(jingle, aktif_rid, simdi)
                    # Geçiş: şarkı song_end_before saniye kalmışsa jingle'ı kuyruğa at
                    sarki_sonu_gecis_tetikle(jingle, aktif_rid, simdi, remaining, ayarlar)
            if ayarlar.get("hourly_announcement_enabled") and simdi.minute == 0:
                anahtar = f'hourly:{simdi:%Y-%m-%d-%H}:00'
                if anahtar not in LAST_FIRED:
                    path = saat_anonsu_bul(ayarlar, simdi)
                    if path:
                        planli_anons = {"name": f"Saat {simdi.hour:02d} anonsu", "paths": [path], "cut_policy": "after"}
                        basarili = jingle_kuyruga_at(planli_anons)
                        if basarili:
                            LAST_FIRED.add(anahtar)
            if biten_rid:
                LAST_MUSIC_FINISH_RID = biten_rid
        except Exception as hata:
            print(f"Jingle zamanlayici hatasi: {hata}")
        time.sleep(1)


def scheduler_baslat():
    global SCHEDULER_STARTED
    if SCHEDULER_STARTED:
        return
    threading.Thread(target=jingle_scheduler, daemon=True).start()
    SCHEDULER_STARTED = True


def saglik_monitoru():
    while True:
        try:
            ayarlar = ayarlari_oku()
            listener_kaydet()
            otomatik_yedek_kontrol(ayarlar)
            if ayarlar.get("auto_restart", True) and not ayarlar.get("manual_stop", False):
                if container_durumu() != "YAYINDA":
                    docker_komut("start")
        except Exception as hata:
            print(f"Saglik monitoru hatasi: {hata}")
        time.sleep(15)


def monitor_baslat():
    global MONITOR_STARTED
    if MONITOR_STARTED:
        return
    threading.Thread(target=saglik_monitoru, daemon=True).start()
    MONITOR_STARTED = True


@app.before_request
def panel_koruma():
    if request.endpoint in {"login", "static", "widget_embed"} or request.path.startswith("/api/"):
        return None
    if PANEL_PASSWORD and not session.get("logged_in"):
        return redirect(url_for("login"))
    return None


@app.route("/")
def index():
    now = aktif_metadata()
    ayarlar = ayarlari_oku()
    backup_status = otomatik_yedek_kontrolu(ayarlar)
    return render_template(
        "index.html",
        durum=container_durumu(),
        stream_url=yayin_linki(),
        now=now,
        next_track=siradaki_sarki_bilgisi(now),
        planned_next=siradaki_planli_playlist(ayarlar),
        history=gecmis_oku()[:PANEL_HISTORY_LIMIT],
        ayarlar=ayarlar,
        health=yayin_sagligi(),
        disk=disk_bilgisi(),
        warnings=panel_uyarilari(ayarlar),
        backup_status=backup_status,
        flow=yayin_akisi_olustur(ayarlar, 10),
    )


@app.route("/yayini-indir.m3u")
def yayini_indir():
    stream = yayin_linki()
    body = f"#EXTM3U\n#EXTINF:-1,{ayarlari_oku().get('station_name', 'Radyo')}\n{stream}\n"
    return Response(
        body,
        mimetype="audio/x-mpegurl",
        headers={"Content-Disposition": "attachment; filename=akkurtcast-yayin.m3u"},
    )


@app.route("/login", methods=["GET", "POST"])
def login():
    hata = ""
    if request.method == "POST":
        if request.form.get("username") == PANEL_USER and request.form.get("password") == PANEL_PASSWORD:
            session["logged_in"] = True
            return redirect(url_for("index"))
        hata = "Kullanıcı adı veya şifre hatalı."
    return render_template("login.html", hata=hata)


@app.route("/sunucu-ayarlari", methods=["POST"])
def sunucu_ayarlari():
    ayarlar = ayarlari_oku()
    ayarlar["station_name"] = request.form.get("station_name", "Radyo Pop").strip() or "Radyo Pop"
    ayarlar["station_genre"] = request.form.get("station_genre", "").strip()
    ayarlar["station_url"] = request.form.get("station_url", "").strip()
    ayarlar["station_description"] = request.form.get("station_description", "").strip()
    ayarlari_yaz(ayarlar)
    liquidsoap_yaz(ayarlar)
    docker_komut("restart")
    return redirect(url_for("index"))


@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))


@app.route("/baslat", methods=["GET", "POST"])
def baslat():
    if request.method != "POST":
        sunucu_islem_kaydi("start-blocked", False, "GET istegi engellendi")
        return redirect(url_for("index"))
    global SCHEDULER_PAUSED_UNTIL, PENDING_AFTER_JINGLES
    ayarlar = ayarlari_oku()
    ayarlar["manual_stop"] = False
    ayarlari_yaz(ayarlar)
    liquidsoap_yaz(ayarlar)
    basarili = docker_komut("start")
    sunucu_islem_kaydi("start", basarili)
    PENDING_AFTER_JINGLES.clear()
    LAST_FIRED.add(f"{dt.datetime.now(ISTANBUL_TZ):%Y-%m-%d-%H-%M}:startup")
    SCHEDULER_PAUSED_UNTIL = dt.datetime.now(ISTANBUL_TZ) + dt.timedelta(seconds=45)
    threading.Timer(3, jingle_kuyruklarini_temizle).start()
    return redirect(url_for("index"))


@app.route("/durdur", methods=["GET", "POST"])
def durdur():
    if request.method != "POST":
        sunucu_islem_kaydi("stop-blocked", False, "GET istegi engellendi")
        return redirect(url_for("index"))
    if request.form.get("stop_confirm") != "DURDUR":
        sunucu_islem_kaydi("stop-blocked", False, "Eksik durdurma onayi")
        return redirect(url_for("index"))
    ayarlar = ayarlari_oku()
    ayarlar["manual_stop"] = True
    ayarlari_yaz(ayarlar)
    basarili = docker_komut("stop")
    sunucu_islem_kaydi("stop", basarili)
    return redirect(url_for("index"))


@app.route("/listeler")
def listeler():
    return redirect(url_for("muzik"))


@app.route("/muzik")
def muzik():
    ayarlar = ayarlari_oku()
    active_playlist = ayarlar.get("active_playlist", "all")
    listeler = muzik_listeleri(ayarlar)
    history = gecmis_oku()
    for liste in listeler:
        liste["resolved_group"] = playlist_gorunum_grubu(liste)
        liste["last_played"] = playlist_son_calma_bilgisi(liste, history)
    saat_anonslari = [liste for liste in listeler if liste["resolved_group"] == "saat"]
    normal_anonslar = [liste for liste in listeler if liste["resolved_group"] == "normal"]
    ana_listeler = [liste for liste in listeler if liste["resolved_group"] == "main"]
    return render_template(
        "muzik.html",
        durum=container_durumu(),
        listeler=listeler,
        oynatma_listeleri=ana_listeler,
        saat_anonslari=saat_anonslari,
        normal_anonslar=normal_anonslar,
        playlist_group_names=ayarlar.get("playlist_group_names", {"normal": "Normal Anonslar", "saat": "Saat Anonsları"}),
        active_playlist=active_playlist,
        available_media=mevcut_medya_dosyalari(),
        pool_report=None,  # AJAX ile yukleniyor: /api/havuz-analizi
        playlist_health=playlist_saglik_kontrolu(ayarlar),
        ayarlar=ayarlar,
    )


@app.route("/api/havuz-analizi")
def api_havuz_analizi():
    from flask import jsonify
    return jsonify(sarki_havuzu_analizi())


@app.route("/api/playlist-parcalari/<playlist_key>")
def api_playlist_parcalari(playlist_key):
    ayarlar = ayarlari_oku()
    if playlist_key != "all" and playlist_key not in ayarlar.get("playlists", {}) and not os.path.isdir(playlist_klasor_yolu(playlist_key, ayarlar)):
        return jsonify({"name": playlist_key, "tracks": [], "total": 0})

    liste_adi = "Tüm Müzikler"
    if playlist_key != "all":
        liste_adi = ayarlar.get("playlists", {}).get(playlist_key, {}).get("name") or playlist_key

    tracks = []
    for index, path in enumerate(muzik_dosyalari(playlist_key), start=1):
        artist, title = dosya_adindan_meta(path)
        rel = path
        try:
            if path.replace("\\", "/").startswith(MUZIK_YOLU.replace("\\", "/")):
                rel = os.path.relpath(path, MUZIK_YOLU).replace("\\", "/")
            elif path.replace("\\", "/").startswith(JINGLE_YOLU.replace("\\", "/")):
                rel = "jingle/" + os.path.relpath(path, JINGLE_YOLU).replace("\\", "/")
        except ValueError:
            rel = os.path.basename(path)
        tracks.append({
            "no": index,
            "name": title or os.path.splitext(os.path.basename(path))[0],
            "artist": artist,
            "path": path,
            "relative": rel,
        })
    return jsonify({"name": liste_adi, "tracks": tracks, "total": len(tracks)})


@app.route("/playlist-sec/<playlist_key>", methods=["POST"])
def playlist_sec(playlist_key):
    ayarlar = ayarlari_oku()
    ayarlar["active_playlist"] = playlist_key
    ayarlari_yaz(ayarlar)
    calan_listeyi_degistir(playlist_key)
    return redirect(url_for("muzik"))


@app.route("/playlist-test/<playlist_key>", methods=["POST"])
def playlist_test(playlist_key):
    ayarlar = ayarlari_oku()
    playlist = ayarlar.get("playlists", {}).get(playlist_key)
    if not playlist:
        return redirect(url_for("muzik"))
    test_playlist = dict(playlist)
    test_playlist["interrupt_other"] = False
    playlist_kuyruga_at(playlist_key, test_playlist, ayarlar)
    return redirect(url_for("muzik"))


@app.route("/playlist-durum/<playlist_key>", methods=["POST"])
def playlist_durum(playlist_key):
    if playlist_key == "all":
        return redirect(url_for("muzik"))
    ayarlar = ayarlari_oku()
    playlist = ayarlar.setdefault("playlists", {}).get(playlist_key)
    if not playlist:
        return redirect(url_for("muzik"))
    playlist["active"] = not bool(playlist.get("active", True))
    playlist["updated_at"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
    ayarlari_yaz(ayarlar)
    aktif_playlisti_yenile(ayarlar)
    return redirect(url_for("muzik"))


@app.route("/playlist-karistir/<playlist_key>", methods=["POST"])
def playlist_karistir(playlist_key):
    ayarlar = ayarlari_oku()
    ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = request.form.get("shuffle") == "on"
    ayarlari_yaz(ayarlar)
    aktif_playlisti_yenile(ayarlar)
    return redirect(url_for("muzik"))


@app.route("/playlist-sira/<playlist_key>/<direction>", methods=["POST"])
def playlist_sira(playlist_key, direction):
    if playlist_key == "all" or direction not in {"up", "down"}:
        return redirect(url_for("muzik"))

    ayarlar = ayarlari_oku()
    mevcutlar = [liste["key"] for liste in muzik_listeleri(ayarlar) if liste["key"] != "all"]
    order = [key for key in ayarlar.get("playlist_order", []) if key in mevcutlar]
    for key in mevcutlar:
        if key not in order:
            order.append(key)

    if playlist_key in order:
        index = order.index(playlist_key)
        hedef = index - 1 if direction == "up" else index + 1
        if 0 <= hedef < len(order):
            order[index], order[hedef] = order[hedef], order[index]
            ayarlar["playlist_order"] = order
            ayarlari_yaz(ayarlar)

    return redirect(url_for("muzik"))


@app.route("/playlist-grup/<playlist_key>", methods=["POST"])
def playlist_grup(playlist_key):
    if playlist_key == "all":
        return redirect(url_for("muzik"))
    grup = request.form.get("display_group", "auto")
    if grup not in {"auto", "main", "normal", "saat"}:
        grup = "auto"
    ayarlar = ayarlari_oku()
    playlist = ayarlar.setdefault("playlists", {}).setdefault(playlist_key, {})
    playlist["display_group"] = grup
    ayarlari_yaz(ayarlar)
    if request.headers.get("X-Requested-With") == "fetch":
        return ("", 204)
    return redirect(url_for("muzik"))


@app.route("/playlist-grup-baslik", methods=["POST"])
def playlist_grup_baslik():
    ayarlar = ayarlari_oku()
    names = ayarlar.setdefault("playlist_group_names", {})
    names["normal"] = request.form.get("normal_name", names.get("normal", "Normal Anonslar")).strip() or "Normal Anonslar"
    names["saat"] = request.form.get("saat_name", names.get("saat", "Saat Anonsları")).strip() or "Saat Anonsları"
    ayarlari_yaz(ayarlar)
    return redirect(url_for("muzik"))


@app.route("/playlist-ekle", methods=["POST"])
def playlist_ekle():
    name = request.form.get("name", "").strip()
    if not name:
        return redirect(url_for("muzik"))

    ayarlar = ayarlari_oku()
    playlist_key = playlist_key_uret(name)
    os.makedirs(playlist_klasor_yolu(playlist_key, ayarlar), exist_ok=True)

    playback_order = request.form.get("playback_order", "shuffle")
    if playback_order == "sequential":
        ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = False
    elif playback_order in {"shuffle", "random"}:
        ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = True

    playlist_cfg = playlist_form_kaydi(playlist_key)
    ayarlar.setdefault("playlists", {})[playlist_key] = playlist_cfg
    order = [key for key in ayarlar.get("playlist_order", []) if key != playlist_key]
    order.append(playlist_key)
    ayarlar["playlist_order"] = order

    start_time = playlist_cfg["schedule"]["start_time"]
    if start_time and playlist_cfg.get("playback_type") in {"general", "advanced"}:
        ayarlar.setdefault("schedules", []).append({
            "id": str(uuid.uuid4()),
            "name": name,
            "playlist": playlist_key,
            "time": start_time,
            "end_time": playlist_cfg["schedule"]["end_time"],
            "start_date": playlist_cfg["schedule"]["start_date"],
            "end_date": playlist_cfg["schedule"]["end_date"],
            "days": playlist_cfg["schedule"]["days"],
            "once": playlist_cfg["schedule"]["once"],
            "active": playlist_cfg["active"],
        })

    ayarlari_yaz(ayarlar)
    muzik_cache_temizle()
    aktif_playlisti_yenile(ayarlar)
    return redirect(url_for("muzik"))


@app.route("/playlist-duzenle/<playlist_key>", methods=["POST"])
def playlist_duzenle(playlist_key):
    if playlist_key == "all":
        return redirect(url_for("muzik"))
    ayarlar = ayarlari_oku()
    playlist_path = playlist_klasor_yolu(playlist_key, ayarlar)
    if not os.path.isdir(playlist_path):
        return redirect(url_for("muzik"))

    existing = ayarlar.setdefault("playlists", {}).get(playlist_key, {})
    playlist_cfg = playlist_form_kaydi(playlist_key, existing)
    ayarlar["playlists"][playlist_key] = playlist_cfg

    playback_order = playlist_cfg.get("playback_order", "shuffle")
    if playback_order == "sequential":
        ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = False
    elif playback_order in {"shuffle", "random"}:
        ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = True

    ayarlar["schedules"] = [rule for rule in ayarlar.get("schedules", []) if rule.get("playlist") != playlist_key]
    start_time = playlist_cfg["schedule"]["start_time"]
    if start_time and playlist_cfg.get("playback_type") in {"general", "advanced"}:
        ayarlar.setdefault("schedules", []).append({
            "id": str(uuid.uuid4()),
            "name": playlist_cfg["name"],
            "playlist": playlist_key,
            "time": start_time,
            "end_time": playlist_cfg["schedule"]["end_time"],
            "start_date": playlist_cfg["schedule"]["start_date"],
            "end_date": playlist_cfg["schedule"]["end_date"],
            "days": playlist_cfg["schedule"]["days"],
            "once": playlist_cfg["schedule"]["once"],
            "active": playlist_cfg["active"],
        })

    ayarlari_yaz(ayarlar)
    aktif_playlisti_yenile(ayarlar)
    return redirect(url_for("muzik"))


@app.route("/playlist-sil/<playlist_key>", methods=["POST"])
def playlist_sil(playlist_key):
    if playlist_key == "all":
        return redirect(url_for("muzik"))
    ayarlar = ayarlari_oku()
    playlist_path = playlist_klasor_yolu(playlist_key, ayarlar)
    if os.path.isdir(playlist_path):
        shutil.rmtree(playlist_path)

    ayarlar.get("playlists", {}).pop(playlist_key, None)
    ayarlar.get("playlist_shuffle", {}).pop(playlist_key, None)
    ayarlar["playlist_order"] = [key for key in ayarlar.get("playlist_order", []) if key != playlist_key]
    ayarlar["schedules"] = [rule for rule in ayarlar.get("schedules", []) if rule.get("playlist") != playlist_key]
    if ayarlar.get("active_playlist") == playlist_key:
        ayarlar["active_playlist"] = "all"
    ayarlari_yaz(ayarlar)
    muzik_cache_temizle()
    aktif_playlisti_yenile(ayarlar)
    return redirect(url_for("muzik"))


@app.route("/playlist-dosya-ekle/<playlist_key>", methods=["POST"])
def playlist_dosya_ekle(playlist_key):
    if playlist_key == "all":
        return redirect(url_for("muzik"))
    ayarlar = ayarlari_oku()
    hedef_klasor = playlist_klasor_yolu(playlist_key, ayarlar)
    os.makedirs(hedef_klasor, exist_ok=True)

    playlist_cfg = ayarlar.setdefault("playlists", {}).setdefault(playlist_key, {
        "id": str(uuid.uuid4()),
        "key": playlist_key,
        "name": playlist_key,
        "active": True,
        "source": "songs",
        "playback_type": "general",
        "playback_order": "shuffle",
        "manual_tracks": [],
    })
    manual_tracks = list(playlist_cfg.get("manual_tracks", []))
    for path in request.form.getlist("selected_media"):
        if path and path not in manual_tracks and os.path.exists(path) and ses_dosyasi_mi(path):
            manual_tracks.append(path)
    playlist_cfg["manual_tracks"] = manual_tracks
    playlist_cfg["updated_at"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")

    for file in request.files.getlist("files"):
        if not file or not file.filename or not ses_dosyasi_mi(file.filename):
            continue
        hedef = benzersiz_dosya_yolu(hedef_klasor, file.filename)
        if hedef:
            file.save(hedef)
    muzik_cache_temizle()
    ayarlari_yaz(ayarlar)
    aktif_playlisti_yenile(ayarlar)
    return redirect(url_for("muzik"))


@app.route("/muzik-ayarlari", methods=["POST"])
def muzik_ayarlari():
    ayarlar = ayarlari_oku()
    eski_volume = str(ayarlar.get("jingle_volume", "100"))
    eski_transition = str(ayarlar.get("jingle_transition", "0"))
    eski_overlap = str(ayarlar.get("jingle_music_overlap", "1.5"))
    ayarlar["playlist_mode"] = request.form.get("playlist_mode", "shuffle")
    ayarlar["artist_gap"] = request.form.get("artist_gap", "3")
    ayarlar["track_repeat_hours"] = request.form.get("track_repeat_hours", "6")
    ayarlar["jingle_volume"] = request.form.get("jingle_volume", "100")
    ayarlar["jingle_transition"] = request.form.get("jingle_transition", ayarlar.get("jingle_transition", "0"))
    ayarlar["jingle_music_overlap"] = request.form.get("jingle_music_overlap", ayarlar.get("jingle_music_overlap", "1.5"))
    ayarlar["auto_restart"] = request.form.get("auto_restart") == "on"
    ayarlar["day_playlist"] = request.form.get("day_playlist", "")
    ayarlar["night_playlist"] = request.form.get("night_playlist", "")
    ayarlar["day_start"] = request.form.get("day_start", "07:00")
    ayarlar["night_start"] = request.form.get("night_start", "22:00")
    ayarlari_yaz(ayarlar)
    if (
        str(ayarlar.get("jingle_volume", "100")) != eski_volume
        or str(ayarlar.get("jingle_transition", "0")) != eski_transition
        or str(ayarlar.get("jingle_music_overlap", "1.5")) != eski_overlap
    ):
        liquidsoap_yaz(ayarlar)
        docker_komut("restart")
    else:
        playlist_yaz(ayarlar.get("active_playlist", "all"), ayarlar)
        liquidsoap_komut("music.reload")
    return redirect(url_for("muzik"))


@app.route("/jingle-ayarlari", methods=["POST"])
def jingle_ayarlari():
    ayarlar = ayarlari_oku()
    ayarlar["jingle_volume"] = request.form.get("jingle_volume", ayarlar.get("jingle_volume", "100"))
    ayarlar["jingle_transition"] = request.form.get("jingle_transition", ayarlar.get("jingle_transition", "0"))
    ayarlar["jingle_music_overlap"] = request.form.get("jingle_music_overlap", ayarlar.get("jingle_music_overlap", "1.5"))
    ayarlar["jingle_duck_volume"] = request.form.get("jingle_duck_volume", ayarlar.get("jingle_duck_volume", "35"))
    try:
        ayarlar["song_end_before"] = float(request.form.get("song_end_before", ayarlar.get("song_end_before", 2.0)))
    except (ValueError, TypeError):
        ayarlar["song_end_before"] = 2.0
    try:
        ayarlar["jingle_end_before"] = float(request.form.get("jingle_end_before", ayarlar.get("jingle_end_before", 2.0)))
    except (ValueError, TypeError):
        ayarlar["jingle_end_before"] = 2.0
    ayarlari_yaz(ayarlar)
    liquidsoap_yaz(ayarlar)
    docker_komut("restart")
    return redirect(url_for("jingle"))


@app.route("/ayarlar")
@app.route("/settings")
def ayarlar_sayfasi():
    return render_template(
        "ayarlar.html",
        ayarlar=ayarlari_oku(),
        presets={
            "natural": "Doğal",
            "pop_radio": "Pop Radyo",
            "bass_strong": "Bass Güçlü",
            "bright": "Parlak Yayın",
            "night_soft": "Gece Yumuşak",
        },
        disk=disk_bilgisi(),
        ftp_host=request.host.split(":")[0],
    )


@app.route("/ayarlar-kaydet", methods=["POST"])
def ayarlar_kaydet():
    ayarlar = ayarlari_oku()
    eski_ftp_password = str(ayarlar.get("ftp_password", "radyo123"))
    ayarlar["station_name"] = request.form.get("station_name", "Radyo Pop")
    ayarlar["station_genre"] = request.form.get("station_genre", "")
    ayarlar["station_url"] = request.form.get("station_url", "")
    ayarlar["station_description"] = request.form.get("station_description", "")
    ayarlar["crossfade_enabled"] = request.form.get("crossfade_enabled") == "on"
    ayarlar["crossfade_duration"] = request.form.get("crossfade_duration", "3")
    ayarlar["fade_in"] = request.form.get("fade_in", "2")
    ayarlar["fade_out"] = request.form.get("fade_out", "2")
    ayarlar["smart_crossfade"] = request.form.get("smart_crossfade") == "on"
    ayarlar["audio_processing_enabled"] = request.form.get("audio_processing_enabled") == "on"
    ayarlar["audio_preset"] = request.form.get("audio_preset", "pop_radio")
    ayarlar["bass_gain"] = request.form.get("bass_gain", "3")
    ayarlar["low_mid_gain"] = request.form.get("low_mid_gain", "-1")
    ayarlar["presence_gain"] = request.form.get("presence_gain", "2")
    ayarlar["air_gain"] = request.form.get("air_gain", "1.5")
    ayarlar["multiband_strength"] = request.form.get("multiband_strength", "2")
    ayarlar["compressor_strength"] = request.form.get("compressor_strength", "4")
    ayarlar["output_gain"] = request.form.get("output_gain", "0")
    ayarlar["limiter_enabled"] = request.form.get("limiter_enabled") == "on"
    ayarlar["normalize_audio"] = request.form.get("normalize_audio") == "on"
    ayarlar["jingle_transition"] = request.form.get("jingle_transition", ayarlar.get("jingle_transition", "0"))
    ayarlar["jingle_music_overlap"] = request.form.get("jingle_music_overlap", ayarlar.get("jingle_music_overlap", "1.5"))
    ayarlar["jingle_duck_volume"] = request.form.get("jingle_duck_volume", ayarlar.get("jingle_duck_volume", "35"))
    ayarlar["static_metadata"] = request.form.get("static_metadata", "")
    ayarlar["empty_playlist_metadata"] = request.form.get("empty_playlist_metadata", "Radyo")
    ayarlar["auto_restart"] = request.form.get("auto_restart") == "on"
    ayarlar["recording_enabled"] = request.form.get("recording_enabled") == "on"
    ayarlar["silence_detection_enabled"] = request.form.get("silence_detection_enabled") == "on"
    ayarlar["silence_seconds"] = request.form.get("silence_seconds", "20")
    ayarlar["silence_threshold"] = request.form.get("silence_threshold", "-45")
    ayarlar["trim_silence_enabled"] = request.form.get("trim_silence_enabled") == "on"
    ayarlar["trim_silence_seconds"] = request.form.get("trim_silence_seconds", "3")
    ayarlar["trim_silence_threshold"] = request.form.get("trim_silence_threshold", ayarlar.get("silence_threshold", "-45"))
    yeni_ftp_password = request.form.get("ftp_password", "").strip()
    if yeni_ftp_password:
        ayarlar["ftp_password"] = yeni_ftp_password
    ayarlari_yaz(ayarlar)
    if str(ayarlar.get("ftp_password", "radyo123")) != eski_ftp_password:
        ftp_servislerini_yenile(ayarlar)
    liquidsoap_yaz(ayarlar)
    docker_komut("restart")
    return redirect(url_for("ayarlar_sayfasi"))


@app.route("/raporlar")
def raporlar():
    kesinti = yayin_kesinti_ozeti()
    listener_history = listener_gecmisi_oku()[-180:]
    return render_template(
        "raporlar.html",
        listeners=listener_sayisi(),
        history=listener_history,
        listener_chart=listener_grafik_verisi(listener_history),
        play_history=gecmis_oku()[:PANEL_HISTORY_LIMIT],
        jingle_audit=list(reversed(jingle_audit_oku(120))),
        jingle_summary=jingle_ozeti(),
        report_summary=yayin_rapor_ozetleri(),
        server_actions=kesinti.get("actions", []),
        interruption=kesinti,
        health=yayin_sagligi(),
        listener_map=dinleyici_haritasi(),
        disk=disk_bilgisi(),
        durum=container_durumu(),
    )


@app.route("/akis")
def yayin_akisi_sayfasi():
    ayarlar = ayarlari_oku()
    return render_template(
        "akis.html",
        durum=container_durumu(),
        flow=yayin_akisi_olustur(ayarlar, 260),
        warnings=panel_uyarilari(ayarlar),
        planned_next=siradaki_planli_playlist(ayarlar),
    )


@app.route("/widgetler")
def widgetler():
    host = panel_base_url()
    return render_template(
        "widgetler.html",
        durum=container_durumu(),
        widget_url=f"{host}/widget",
        api_url=f"{host}/api/now-playing",
        stream_url=yayin_linki(),
        ayarlar=ayarlari_oku(),
    )


@app.route("/widget")
def widget_embed():
    style = request.args.get("style", "classic")
    if style not in {"classic", "compact", "bar", "showcase"}:
        style = "classic"
    theme = request.args.get("theme", "dark")
    if theme not in {"dark", "light", "neon", "minimal"}:
        theme = "dark"
    return render_template("widget_embed.html", stream_url=yayin_linki(), ayarlar=ayarlari_oku(), style=style, theme=theme)


@app.route("/takvim")
def takvim():
    ayarlar = ayarlari_oku()
    listeler = muzik_listeleri(ayarlar)
    planned_next = siradaki_planli_playlist(ayarlar)
    return render_template(
        "takvim.html",
        durum=container_durumu(),
        planned_next=planned_next,
        schedules=ayarlar.get("schedules", []),
        listeler=listeler,
        upcoming=yaklasan_yayin_akisi(ayarlar, 180),
        conflicts=yayin_akisi_cakismalari(ayarlar, 180),
        announcement_history=anons_gecmisi(80),
        playlist_health=playlist_saglik_kontrolu(ayarlar),
        rule_simulation=kural_simulasyonu(ayarlar, 24, 140),
        playlist_stats=playlist_istatistikleri(ayarlar),
        hour_playlists=[liste for liste in listeler if saat_anonsu_playlisti_mi(liste)],
        cleanup_suggestions=playlist_temizlik_onerileri(ayarlar),
        gunler=[("0", "Pzt"), ("1", "Sal"), ("2", "Çar"), ("3", "Per"), ("4", "Cum"), ("5", "Cmt"), ("6", "Paz")],
        gun_adlari={"0": "Pzt", "1": "Sal", "2": "Çar", "3": "Per", "4": "Cum", "5": "Cmt", "6": "Paz"},
    )


@app.route("/takvim-ekle", methods=["POST"])
def takvim_ekle():
    ayarlar = ayarlari_oku()
    playlist = request.form.get("playlist", "")
    zaman = request.form.get("time", "")
    if playlist and zaman:
        ayarlar.setdefault("schedules", []).append({
            "id": str(uuid.uuid4()),
            "name": request.form.get("name") or playlist,
            "playlist": playlist,
            "time": zaman,
            "days": request.form.getlist("days"),
            "active": request.form.get("active") == "on",
        })
        ayarlari_yaz(ayarlar)
    return redirect(url_for("takvim"))


@app.route("/takvim-sil/<schedule_id>", methods=["POST"])
def takvim_sil(schedule_id):
    ayarlar = ayarlari_oku()
    ayarlar["schedules"] = [rule for rule in ayarlar.get("schedules", []) if rule.get("id") != schedule_id]
    ayarlari_yaz(ayarlar)
    return redirect(url_for("takvim"))


@app.route("/saat-anonslari-olustur", methods=["POST"])
def saat_anonslari_olustur():
    ayarlar = ayarlari_oku()
    try:
        start_hour = max(0, min(23, int(request.form.get("start_hour", "7"))))
        end_hour = max(0, min(23, int(request.form.get("end_hour", "23"))))
        minute = max(0, min(59, int(request.form.get("minute", "0"))))
    except ValueError:
        return redirect(url_for("takvim"))
    if start_hour > end_hour:
        start_hour, end_hour = end_hour, start_hour
    playlists = ayarlar.setdefault("playlists", {})
    order = [key for key in ayarlar.get("playlist_order", []) if key in playlists]
    created_at = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
    for hour in range(start_hour, end_hour + 1):
        key = f"saat_{hour:02d}"
        folder_path = saat_anons_playlist_klasoru_hazirla(key)
        existing = playlists.get(key, {})
        track_path = saat_anons_dosya_yolu(hour)
        playlists[key] = {
            **existing,
            "id": existing.get("id") or str(uuid.uuid4()),
            "key": key,
            "name": f"Saat {hour:02d} Anonsu",
            "active": True,
            "description": "Hızlı saat anonsu oluşturucu ile eklendi.",
            "source": "songs",
            "remote_url": "",
            "playback_type": "once_per_hour",
            "playback_order": "shuffle",
            "weight": existing.get("weight", "3"),
            "interval_value": str(minute),
            "include_requests": False,
            "avoid_duplicates": True,
            "show_on_public": False,
            "hide_metadata": False,
            "interrupt_other": False,
            "folder_path": folder_path,
            "scheduled_single_track": True,
            "merge": False,
            "prioritize_requests": False,
            "jingle_group": True,
            "manual_tracks": [track_path],
            "display_group": "saat",
            "schedule": {
                "start_time": f"{hour:02d}:{minute:02d}",
                "end_time": f"{hour:02d}:{min(59, minute + 1):02d}",
                "start_date": "",
                "end_date": "",
                "days": [],
                "once": False,
            },
            "created_at": existing.get("created_at") or created_at,
            "updated_at": created_at,
        }
        ayarlar.setdefault("playlist_shuffle", {})[key] = True
        if key not in order:
            order.append(key)
    ayarlar["playlist_order"] = order
    ayarlari_yaz(ayarlar)
    return redirect(url_for("takvim"))


@app.route("/saat-anonslari-toplu", methods=["POST"])
def saat_anonslari_toplu():
    ayarlar = ayarlari_oku()
    action = request.form.get("bulk_action", "minute")
    try:
        minute = max(0, min(59, int(request.form.get("minute", "0"))))
    except ValueError:
        minute = 0
    for key, playlist in ayarlar.get("playlists", {}).items():
        if not saat_anonsu_playlisti_mi({"key": key, **playlist}):
            continue
        if action == "active":
            playlist["active"] = True
        elif action == "passive":
            playlist["active"] = False
        elif action == "minute":
            playlist["interval_value"] = str(minute)
            schedule = playlist.setdefault("schedule", {})
            start_time = schedule.get("start_time", "00:00")
            try:
                hour = dt.time.fromisoformat(start_time).hour
            except ValueError:
                hour = 0
            schedule["start_time"] = f"{hour:02d}:{minute:02d}"
            schedule["end_time"] = f"{hour:02d}:{min(59, minute + 1):02d}"
        playlist["updated_at"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
    ayarlari_yaz(ayarlar)
    return redirect(url_for("takvim"))


@app.route("/playlist-temizlik", methods=["POST"])
def playlist_temizlik():
    ayarlar = ayarlari_oku()
    action = request.form.get("cleanup_action")
    for key, playlist in ayarlar.get("playlists", {}).items():
        if action == "deactivate_empty" and playlist.get("active", True) and not muzik_dosyalari(key):
            playlist["active"] = False
            playlist["updated_at"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
        elif action == "clear_missing":
            manual_tracks = [path for path in playlist.get("manual_tracks", []) if os.path.exists(path)]
            if len(manual_tracks) != len(playlist.get("manual_tracks", [])):
                playlist["manual_tracks"] = manual_tracks
                playlist["updated_at"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
    ayarlari_yaz(ayarlar)
    return redirect(url_for("takvim"))


@app.route("/otomasyon")
def otomasyon():
    ayarlar = ayarlari_oku()
    query = request.args.get("q", "").strip().lower()
    track_query = request.args.get("track_q", "").strip().lower()
    scan_quality = request.args.get("scan") == "quality"
    history = gecmis_oku()
    if query:
        history = [
            item for item in history
            if query in (item.get("title", "") + " " + item.get("artist", "") + " " + item.get("filename", "")).lower()
        ]
    tracks = tum_muzik_dosyalari(limit=400 if track_query else 120)
    if track_query:
        tracks = [track for track in tracks if track_query in track.get("name", "").lower()]
    tracks = tracks[:120]
    return render_template(
        "otomasyon.html",
        ayarlar=ayarlar,
        jingle_dosyalari=jingle_dosyalari(),
        muzik_dosyalari=tracks,
        folders=klasorler("jingle"),
        ad_blocks=ayarlar.get("ad_blocks", []),
        banned=set(ayarlar.get("banned_tracks", [])),
        favorites=set(ayarlar.get("favorite_tracks", [])),
        flow=yayin_akisi(ayarlar),
        upcoming=yaklasan_yayin_akisi(ayarlar),
        jingle_audit=list(reversed(jingle_audit_oku(60))),
        health=yayin_sagligi(),
        history=history[:100],
        query=query,
        track_query=track_query,
        quality=kalite_taramasi() if scan_quality else [],
        scan_quality=scan_quality,
        gunler=[("0", "Pzt"), ("1", "Sal"), ("2", "Çar"), ("3", "Per"), ("4", "Cum"), ("5", "Cmt"), ("6", "Paz")],
    )


@app.route("/otomasyon-ayar", methods=["POST"])
def otomasyon_ayar():
    ayarlar = ayarlari_oku()
    ayarlar["hourly_announcement_enabled"] = False
    ayarlar["hourly_announcement_folder"] = ""
    ayarlar["ad_blocks"] = []
    ayarlar["auto_backup_enabled"] = request.form.get("auto_backup_enabled") == "on"
    ayarlar["backup_keep_days"] = request.form.get("backup_keep_days", "14")
    ayarlar["recording_keep_days"] = request.form.get("recording_keep_days", "14")
    priority = request.form.getlist("jingle_priority")
    if priority:
        ayarlar["jingle_priority"] = priority
    ayarlari_yaz(ayarlar)
    return redirect(url_for("otomasyon"))


@app.route("/reklam-blok-ekle", methods=["POST"])
def reklam_blok_ekle():
    paths = request.form.getlist("paths")
    if not paths:
        return redirect(url_for("otomasyon"))
    ayarlar = ayarlari_oku()
    ayarlar.setdefault("ad_blocks", []).append({
        "id": str(uuid.uuid4()),
        "name": request.form.get("name") or "Reklam bloğu",
        "paths": paths,
        "hour": request.form.get("hour", ""),
        "minute": request.form.get("minute", "0"),
        "days": request.form.getlist("days"),
        "cut_policy": request.form.get("cut_policy", "after"),
        "active": request.form.get("active") == "on",
    })
    ayarlari_yaz(ayarlar)
    return redirect(url_for("otomasyon"))


@app.route("/reklam-blok-sil/<block_id>", methods=["POST"])
def reklam_blok_sil(block_id):
    ayarlar = ayarlari_oku()
    ayarlar["ad_blocks"] = [block for block in ayarlar.get("ad_blocks", []) if block.get("id") != block_id]
    ayarlari_yaz(ayarlar)
    return redirect(url_for("otomasyon"))


@app.route("/track-isaretle", methods=["POST"])
def track_isaretle():
    ayarlar = ayarlari_oku()
    path = request.form.get("path", "")
    action = request.form.get("action", "")
    if path:
        if action == "ban":
            ayarlar.setdefault("banned_tracks", [])
            if path not in ayarlar["banned_tracks"]:
                ayarlar["banned_tracks"].append(path)
        elif action == "unban":
            ayarlar["banned_tracks"] = [item for item in ayarlar.get("banned_tracks", []) if item != path]
        elif action == "favorite":
            ayarlar.setdefault("favorite_tracks", [])
            if path not in ayarlar["favorite_tracks"]:
                ayarlar["favorite_tracks"].append(path)
        elif action == "unfavorite":
            ayarlar["favorite_tracks"] = [item for item in ayarlar.get("favorite_tracks", []) if item != path]
        ayarlari_yaz(ayarlar)
        playlist_yaz(ayarlar.get("active_playlist", "all"), ayarlar)
        liquidsoap_komut("music.reload")
    return redirect(request.form.get("next") or url_for("otomasyon"))


@app.route("/acil-anons", methods=["POST"])
def acil_anons():
    path = request.form.get("path", "")
    if path:
        jingle_kuyruga_at({"name": "Acil anons", "paths": [path], "cut_policy": request.form.get("cut_policy", "cut")})
    return redirect(request.form.get("next") or url_for("otomasyon"))


@app.route("/naklen-yayin")
def naklen_yayin():
    ayarlar = ayarlari_oku()
    return render_template(
        "naklen_yayin.html",
        durum=container_durumu(),
        ayarlar=ayarlar,
        relays=ayarlar.get("live_relays", []),
        active_id=ayarlar.get("active_live_relay_id", ""),
        volume_options=yuzde_secenekleri(),
    )


@app.route("/naklen-yayin-ekle", methods=["POST"])
def naklen_yayin_ekle():
    ayarlar = ayarlari_oku()
    relay = {
        "id": request.form.get("id") or str(uuid.uuid4()),
        "name": request.form.get("name", "").strip() or "Naklen Yayın",
        "url": request.form.get("url", "").strip(),
        "active": request.form.get("active") == "on",
        "volume_adjust": int(request.form.get("volume_adjust", "0") or 0),
        "image": request.form.get("current_image", ""),
        "created_at": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
    }
    uploaded = panel_gorsel_kaydet(request.files.get("image"), "naklen")
    if uploaded:
        relay["image"] = uploaded
    relays = [item for item in ayarlar.get("live_relays", []) if item.get("id") != relay["id"]]
    relays.append(relay)
    ayarlar["live_relays"] = relays
    ayarlari_yaz(ayarlar)
    return redirect(url_for("naklen_yayin"))


@app.route("/naklen-yayin-yayina-al/<relay_id>", methods=["POST"])
def naklen_yayin_yayina_al(relay_id):
    ayarlar = ayarlari_oku()
    relay = live_relay_bul(ayarlar, relay_id)
    if relay and relay.get("active", True) and live_relay_yayina_al(relay):
        ayarlar["active_live_relay_id"] = relay_id
        relay["last_started"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
        ayarlari_yaz(ayarlar)
    return redirect(url_for("naklen_yayin"))


@app.route("/naklen-yayin-durdur", methods=["POST"])
def naklen_yayin_durdur_route():
    ayarlar = ayarlari_oku()
    live_relay_durdur()
    ayarlar["active_live_relay_id"] = ""
    ayarlari_yaz(ayarlar)
    return redirect(url_for("naklen_yayin"))


@app.route("/naklen-yayin-sil/<relay_id>", methods=["POST"])
def naklen_yayin_sil(relay_id):
    ayarlar = ayarlari_oku()
    ayarlar["live_relays"] = [item for item in ayarlar.get("live_relays", []) if item.get("id") != relay_id]
    live_relay_durdur()
    ayarlar["active_live_relay_id"] = ""
    ayarlari_yaz(ayarlar)
    return redirect(url_for("naklen_yayin"))


@app.route("/dj")
def djler():
    ayarlar = ayarlari_oku()
    dj_slotlari_hazirla(ayarlar)
    ayarlari_yaz(ayarlar)
    djs = ayarlar.get("djs", [])
    for dj in djs:
        slot = dj_slot_bul(ayarlar, dj.get("port"))
        if slot:
            dj["mount"] = slot.get("mount")
            dj["password"] = slot.get("password")
    return render_template(
        "dj.html",
        durum=container_durumu(),
        ayarlar=ayarlar,
        djs=djs,
        slots=ayarlar.get("dj_slots", []),
        volume_options=yuzde_secenekleri(),
        next_port=(dj_bos_slot(ayarlar) or {}).get("port", 1290),
    )


@app.route("/dj-ekle", methods=["POST"])
def dj_ekle():
    ayarlar = ayarlari_oku()
    dj_id = request.form.get("id") or str(uuid.uuid4())
    password = str(uuid.uuid4()).replace("-", "")[:8]
    try:
        port = int(request.form.get("port", "") or (dj_bos_slot(ayarlar) or {}).get("port", 1290))
    except ValueError:
        port = (dj_bos_slot(ayarlar) or {}).get("port", 1290)
    slot = dj_slot_bul(ayarlar, port) or dj_bos_slot(ayarlar)
    port = int(slot.get("port", port))
    dj = {
        "id": dj_id,
        "name": request.form.get("name", "").strip() or "Remote DJ",
        "mode": request.form.get("mode", "desktop"),
        "active": request.form.get("active") == "on",
        "password": slot.get("password", password),
        "port": port,
        "mount": slot.get("mount", f"/dj-{port}"),
        "volume_adjust": int(request.form.get("volume_adjust", "0") or 0),
        "record": request.form.get("record") == "on",
        "image": request.form.get("current_image", ""),
        "created_at": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
    }
    uploaded = panel_gorsel_kaydet(request.files.get("image"), "dj")
    if uploaded:
        dj["image"] = uploaded
    djs = [item for item in ayarlar.get("djs", []) if item.get("id") != dj_id]
    djs.append(dj)
    ayarlar["djs"] = djs
    ayarlari_yaz(ayarlar)
    return redirect(url_for("djler"))


@app.route("/dj-sil/<dj_id>", methods=["POST"])
def dj_sil(dj_id):
    ayarlar = ayarlari_oku()
    ayarlar["djs"] = [item for item in ayarlar.get("djs", []) if item.get("id") != dj_id]
    ayarlari_yaz(ayarlar)
    return redirect(url_for("djler"))


@app.route("/haber-bulteni")
def haber_bulteni():
    ayarlar = haber_ayarlari_oku()
    show_haber_ayarlar = show_haber_ayarlari_oku()
    preview_items = []
    preview_error = ""
    if request.args.get("preview") == "1":
        if ayarlar.get("voice_provider") == "audio_rss":
            preview_items, preview_error = haber_podcast_rss_oku(ayarlar)
        else:
            preview_items, preview_error = haber_rss_oku(ayarlar)
    return render_template(
        "haber_bulteni.html",
        durum=container_durumu(),
        ayarlar=ayarlar,
        kaynaklar=haber_kaynaklari(),
        podcast_kaynaklari=haber_podcast_kaynaklari(),
        jingle_dosyalari=jingle_dosyalari(),
        haber_muzikleri=haber_muzikleri(),
        haber_ciktilari=haber_cikti_dosyalari(),
        preview_items=preview_items,
        preview_error=preview_error,
        taslak=haber_taslak_oku(),
        show_haber=show_haber_ayarlar,
        show_haber_durum=show_haber_durum_bilgisi(),
        show_haber_saatler=show_haber_saat_secenekleri(),
        show_haber_custom_times=[
            item for item in show_haber_ayarlar.get("schedule_times", []) if item not in show_haber_saat_secenekleri()
        ],
        show_haber_play_times=show_haber_yayin_saatleri(
            show_haber_ayarlar.get("schedule_times", []),
            (show_haber_ayarlar.get("panel") or {}).get("play_offset_minutes", 5),
        ),
        show_haber_medya=mevcut_medya_dosyalari(),
        saat_dakika=saat_dakika_secenekleri(),
    )


@app.route("/haber-bulteni-kaydet", methods=["POST"])
def haber_bulteni_kaydet():
    ayarlar = haber_ayarlari_oku()
    ayarlar["enabled"] = request.form.get("enabled") == "on"
    ayarlar["source"] = request.form.get("source", "trt")
    ayarlar["custom_rss"] = request.form.get("custom_rss", "").strip()
    ayarlar["item_count"] = max(1, min(12, int(request.form.get("item_count", "5") or 5)))
    ayarlar["times"] = request.form.get("times", "")
    ayarlar["intro_jingle"] = request.form.get("intro_jingle", "")
    ayarlar["outro_jingle"] = request.form.get("outro_jingle", "")
    ayarlar["background_music"] = request.form.get("background_music", "")
    ayarlar["background_volume"] = max(0, min(60, int(request.form.get("background_volume", "18") or 18)))
    ayarlar["voice_provider"] = request.form.get("voice_provider", "text_tts")
    ayarlar["edge_voice"] = request.form.get("edge_voice", "").strip() or "tr-TR-EmelNeural"
    ayarlar["openai_api_key"] = request.form.get("openai_api_key", "").strip()
    ayarlar["openai_model"] = request.form.get("openai_model", "").strip() or "gpt-4o-mini-tts"
    ayarlar["openai_voice"] = request.form.get("openai_voice", "").strip() or "alloy"
    ayarlar["elevenlabs_api_key"] = request.form.get("elevenlabs_api_key", "").strip()
    ayarlar["elevenlabs_voice_id"] = request.form.get("elevenlabs_voice_id", "").strip()
    ayarlar["podcast_source"] = request.form.get("podcast_source", "custom")
    ayarlar["podcast_rss"] = request.form.get("podcast_rss", "").strip()
    ayarlar["cleanup_days"] = max(1, min(365, int(request.form.get("cleanup_days", "14") or 14)))
    haber_ayarlari_yaz(ayarlar)
    return redirect(url_for("haber_bulteni"))


@app.route("/show-haber-kaydet", methods=["POST"])
def show_haber_kaydet():
    ayarlar = show_haber_ayarlari_oku()
    ayarlar["stream_url"] = request.form.get("stream_url", "").strip() or "https://windows.showradyo.com.tr"
    selected_times = request.form.getlist("schedule_times")
    free_times = saat_listesi_parse(request.form.get("custom_schedule_times", ""))
    schedule_times = []
    for item in selected_times + free_times:
        if re.match(r"^\d{2}:\d{2}$", item) and item not in schedule_times:
            schedule_times.append(item)
    ayarlar["schedule_times"] = schedule_times or ["08:00", "12:00", "13:00"]
    ayarlar["start_padding_seconds"] = max(0, min(60, int(request.form.get("start_padding_seconds", "8") or 8)))
    ayarlar["pre_speech_keep_seconds"] = max(0, min(30, int(request.form.get("pre_speech_keep_seconds", "4") or 4)))
    ayarlar["max_duration_seconds"] = max(60, min(600, int(request.form.get("max_duration_seconds", "240") or 240)))
    ayarlar["require_stop_phrase"] = True
    ayarlar["min_elapsed_before_stop_seconds"] = max(0, min(600, int(request.form.get("min_elapsed_before_stop_seconds", "60") or 60)))
    ayarlar["stop_phrases"] = [line.strip() for line in request.form.get("stop_phrases", "").splitlines() if line.strip()]
    ayarlar["stop_required_terms"] = [item.strip() for item in request.form.get("stop_required_terms", "").split(",") if item.strip()]

    def form_float(name, default, minimum=0.0, maximum=2.0):
        try:
            value = float(request.form.get(name, str(default)) or default)
        except (TypeError, ValueError):
            value = float(default)
        return max(minimum, min(maximum, value))

    wrap = ayarlar.setdefault("wrap_audio", {})
    wrap["enabled"] = request.form.get("wrap_enabled") == "on"
    wrap["intro_path"] = request.form.get("intro_path", "")
    wrap["intro_max_seconds"] = max(0, min(600, int(request.form.get("intro_max_seconds", "0") or 0)))
    wrap["outro_path"] = request.form.get("outro_path", "")
    wrap["outro_max_seconds"] = max(0, min(600, int(request.form.get("outro_max_seconds", "0") or 0)))
    try:
        fallback_crossfade = float(wrap.get("crossfade_seconds", 0.35) or 0.35)
    except (TypeError, ValueError):
        fallback_crossfade = 0.35
    fallback_crossfade = max(0.0, min(2.0, fallback_crossfade))
    wrap["intro_crossfade_seconds"] = form_float(
        "intro_crossfade_seconds",
        wrap.get("intro_crossfade_seconds", fallback_crossfade),
    )
    wrap["outro_crossfade_seconds"] = form_float(
        "outro_crossfade_seconds",
        wrap.get("outro_crossfade_seconds", fallback_crossfade),
    )
    wrap["crossfade_seconds"] = fallback_crossfade

    play_offset = max(1, min(60, int(request.form.get("play_offset_minutes", "5") or 5)))
    play_times = show_haber_yayin_saatleri(ayarlar["schedule_times"], play_offset)
    panel = ayarlar.setdefault("panel", {})
    panel["play_offset_minutes"] = play_offset
    panel["play_times"] = play_times
    panel["play_time"] = play_times[0]
    show_haber_playlist_guncelle(ayarlar["schedule_times"], play_offset)
    show_haber_ayarlari_yaz(ayarlar)
    return redirect(url_for("haber_bulteni"))


@app.route("/show-haber-manuel-kayit", methods=["POST"])
def show_haber_manuel_kayit():
    os.makedirs(STATE_YOLU, exist_ok=True)
    with open(SHOW_HABER_TRIGGER_YOLU, "w", encoding="utf-8") as f:
        json.dump({"reason": "akkurtcast-panel", "created_at": dt.datetime.now(ISTANBUL_TZ).isoformat()}, f)
    return redirect(url_for("haber_bulteni"))


@app.route("/haber-muzigi-yukle", methods=["POST"])
def haber_muzigi_yukle():
    os.makedirs(HABER_YOLU, exist_ok=True)
    for file in request.files.getlist("files"):
        if file and file.filename and ses_dosyasi_mi(file.filename):
            file.save(os.path.join(HABER_YOLU, secure_filename(file.filename)))
    return redirect(url_for("haber_bulteni"))


@app.route("/haber-bulteni-taslak", methods=["POST"])
def haber_bulteni_taslak():
    ayarlar = haber_ayarlari_oku()
    items, error = haber_rss_oku(ayarlar)
    if items and not error:
        metin = haber_bulteni_metni_olustur(items)
        os.makedirs(STATE_YOLU, exist_ok=True)
        with open(HABER_TASLAK_YOLU, "w", encoding="utf-8") as f:
            f.write(metin)
        ayarlar["last_generated"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
        haber_ayarlari_yaz(ayarlar)
    return redirect(url_for("haber_bulteni", preview="1"))


@app.route("/haber-bulteni-sesli-uret", methods=["POST"])
def haber_bulteni_sesli_uret_route():
    ayarlar = haber_ayarlari_oku()
    try:
        final_path = haber_bulteni_sesli_uret(ayarlar)
        ayarlar["last_audio"] = rel_to_container_path(os.path.dirname(final_path), os.path.basename(final_path))
        ayarlar["last_audio_name"] = os.path.basename(final_path)
        ayarlar["last_generated"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
        ayarlar["last_error"] = ""
    except Exception as exc:
        ayarlar["last_error"] = str(exc)
    haber_ayarlari_yaz(ayarlar)
    return redirect(url_for("haber_bulteni", preview="1"))


@app.route("/haber-bulteni-yayina-al", methods=["POST"])
def haber_bulteni_yayina_al():
    path = request.form.get("path", "")
    if path and os.path.isfile(path):
        jingle_kuyruga_at({
            "name": "Haber Bülteni",
            "paths": [path],
            "cut_policy": request.form.get("cut_policy", "after"),
            "custom_artist": "Radyo Pop",
            "custom_title": "Haber Bülteni",
            "save_history": True,
            "allow_stack": True,
        })
    return redirect(url_for("haber_bulteni"))


@app.route("/haber-bulteni-temizle", methods=["POST"])
def haber_bulteni_temizle_route():
    ayarlar = haber_ayarlari_oku()
    days = request.form.get("cleanup_days") or ayarlar.get("cleanup_days", 14)
    delete_all = request.form.get("delete_all") == "on"
    deleted = haber_cikti_temizle(days=days, delete_all=delete_all)
    ayarlar["cleanup_days"] = max(1, min(365, int(days or 14)))
    ayarlar["last_error"] = f"{deleted} eski haber dosyası silindi." if deleted else "Silinecek eski haber dosyası bulunamadı."
    haber_ayarlari_yaz(ayarlar)
    return redirect(url_for("haber_bulteni"))


@app.route("/mobil")
def mobil():
    return render_template(
        "mobil.html",
        durum=container_durumu(),
        now=aktif_metadata(),
        listeners=listener_sayisi(),
        jingle_dosyalari=jingle_dosyalari()[:80],
    )


@app.route("/yedek")
def yedek():
    ayarlar = ayarlari_oku()
    return render_template("yedek.html", backups=yedek_listesi(), recordings=kayit_listesi(), ayarlar=ayarlar)


@app.route("/yedek-al", methods=["POST"])
def yedek_al():
    yedek_olustur()
    return redirect(url_for("yedek"))


@app.route("/yedek-otomatik-ayar", methods=["POST"])
def yedek_otomatik_ayar():
    ayarlar = ayarlari_oku()
    ayarlar["auto_backup_enabled"] = request.form.get("auto_backup_enabled") == "on"
    ayarlari_yaz(ayarlar)
    return redirect(url_for("yedek"))


@app.route("/yedek-indir/<name>")
def yedek_indir(name):
    safe_name = secure_filename(name)
    path = os.path.join(BACKUP_YOLU, safe_name)
    if not os.path.isfile(path):
        return Response("Bulunamadi", status=404)
    return send_file(path, as_attachment=True)


@app.route("/yedek-sil/<name>", methods=["POST"])
def yedek_sil(name):
    yedek_sil_dosyasi(name)
    return redirect(url_for("yedek"))


@app.route("/kayit-indir/<path:relpath>")
def kayit_indir(relpath):
    if relpath.startswith("media/recordings/"):
        kayit_kok = os.path.abspath(os.path.join(BASE_PATH, "media", "recordings"))
        alt_yol = relpath[len("media/recordings/"):]
    else:
        kayit_kok = os.path.abspath(os.path.join(BASE_PATH, "recordings"))
        alt_yol = relpath
    path = os.path.abspath(os.path.join(kayit_kok, alt_yol))
    if not path.startswith(kayit_kok + os.sep) or not os.path.isfile(path):
        return Response("Bulunamadi", status=404)
    return send_file(path, as_attachment=True)


@app.route("/kayit-sil/<path:relpath>", methods=["POST"])
def kayit_sil(relpath):
    if relpath.startswith("media/recordings/"):
        kayit_kok = os.path.abspath(os.path.join(BASE_PATH, "media", "recordings"))
        alt_yol = relpath[len("media/recordings/"):]
    else:
        kayit_kok = os.path.abspath(os.path.join(BASE_PATH, "recordings"))
        alt_yol = relpath
    path = os.path.abspath(os.path.join(kayit_kok, alt_yol))
    if path.startswith(kayit_kok + os.sep) and os.path.isfile(path):
        os.remove(path)
    return redirect(url_for("yedek"))

@app.route("/jingle")
@app.route("/jingles")
def jingle():
    ayarlar = ayarlari_oku()
    if saat_anonsu_dosya_baglarini_duzelt(ayarlar):
        ayarlari_yaz(ayarlar)
    return render_template(
        "jingle.html",
        durum=container_durumu(),
        jingles=ayarlar.get("jingles", []),
        jingle_groups=jingle_playlistleri(ayarlar),
        ayarlar=ayarlar,
        jingle_status=jingle_status_oku(),
        jingle_dosyalari=jingle_dosyalari(),
        available_media=mevcut_medya_dosyalari(),
        gunler=[("0", "Pzt"), ("1", "Sal"), ("2", "Çar"), ("3", "Per"), ("4", "Cum"), ("5", "Cmt"), ("6", "Paz")],
        gun_adlari={"0": "Pzt", "1": "Sal", "2": "Çar", "3": "Per", "4": "Cum", "5": "Cmt", "6": "Paz"},
    )


def jingle_grup_form_kaydi(playlist_key, existing=None):
    existing = existing or {}
    playback_type = request.form.get("playback_type", "once_per_x_songs")
    interval_value = request.form.get("interval_value", "1")
    start_time = request.form.get("start_time", "")
    if playback_type == "once_per_hour" and start_time:
        try:
            interval_value = str(dt.time.fromisoformat(start_time).minute)
        except ValueError:
            pass
    selected_media = []
    for path in request.form.getlist("selected_media"):
        if path and path not in selected_media and os.path.exists(path) and ses_dosyasi_mi(path):
            selected_media.append(path)
    if not selected_media:
        selected_media = existing.get("manual_tracks", [])
    return {
        "id": existing.get("id") or str(uuid.uuid4()),
        "key": playlist_key,
        "name": request.form.get("name", "").strip() or existing.get("name") or playlist_key,
        "active": request.form.get("active") == "on",
        "description": request.form.get("description", "").strip(),
        "source": "songs",
        "remote_url": "",
        "playback_type": playback_type,
        "playback_order": request.form.get("playback_order", "shuffle"),
        "weight": existing.get("weight", "3"),
        "interval_value": interval_value,
        "include_requests": False,
        "avoid_duplicates": True,
        "show_on_public": False,
        "hide_metadata": False,
        "interrupt_other": request.form.get("interrupt_other") == "on",
        "scheduled_single_track": True,
        "merge": False,
        "prioritize_requests": False,
        "jingle_group": True,
        "manual_tracks": selected_media,
        "schedule": {
            "start_time": start_time,
            "end_time": request.form.get("end_time", ""),
            "start_date": "",
            "end_date": "",
            "days": request.form.getlist("days"),
            "once": False,
        },
        "created_at": existing.get("created_at") or dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
        "updated_at": dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S"),
    }


def jingle_playlist_mi(playlist_key, playlist):
    return bool(playlist.get("jingle_group")) or "jingle" in playlist_key.lower() or "jingle" in str(playlist.get("name", "")).lower()


@app.route("/jingle-ekle", methods=["POST"])
def jingle_ekle():
    if request.form.get("playlist_group") == "1":
        name = request.form.get("name", "").strip() or "Jingle Grubu"
        ayarlar = ayarlari_oku()
        playlist_key = playlist_key_uret(name)
        os.makedirs(os.path.join(MUZIK_YOLU, playlist_key), exist_ok=True)
        playlist_cfg = jingle_grup_form_kaydi(playlist_key)
        ayarlar.setdefault("playlists", {})[playlist_key] = playlist_cfg
        if playlist_cfg.get("playback_order") == "sequential":
            ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = False
        else:
            ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = True
        ayarlari_yaz(ayarlar)
        muzik_cache_temizle()
        aktif_playlisti_yenile(ayarlar)
        return redirect(url_for("jingle"))

    path = request.form.get("path", "")
    rotation_paths = [p for p in request.form.getlist("rotation_paths") if p]
    block_paths = [p for p in request.form.getlist("block_paths") if p]
    mode = request.form.get("mode", "time")

    if mode == "block":
        paths = block_paths
        path = paths[0] if paths else ""
        rotate = False
    elif rotation_paths:
        paths = rotation_paths
        path = paths[0]
        rotate = len(paths) > 1
    else:
        paths = [path] if path else []
        rotate = False

    if not path:
        return redirect(url_for("jingle"))

    ayarlar = ayarlari_oku()
    kayit = {
        "id": str(uuid.uuid4()),
        "name": request.form.get("name") or ("Reklam bloğu" if mode == "block" else ("Jingle rotasyonu" if rotate else os.path.basename(path))),
        "path": path,
        "paths": paths,
        "rotate": rotate,
        "mode": mode,
        "active": request.form.get("active") == "on",
        "days": request.form.getlist("days"),
        "start_time": request.form.get("start_time", ""),
        "end_time": request.form.get("end_time", ""),
        "hour": request.form.get("hour", ""),
        "minute": request.form.get("minute", "0"),
        "every_songs": request.form.get("every_songs", "3"),
        "cut_policy": request.form.get("cut_policy", "after"),
        "save_history": request.form.get("save_history") == "on",
        "custom_artist": request.form.get("custom_artist", "").strip(),
        "custom_title": request.form.get("custom_title", "").strip(),
    }
    ayarlar.setdefault("jingles", []).append(kayit)
    if mode == "songs":
        SONG_COUNTERS[kayit["id"]] = 0
        LAST_SONG_COUNTER_TRACK.pop(kayit["id"], None)
        LAST_SONG_JINGLE_TRACK.pop(kayit["id"], None)
        jingle_status_yaz(last_added=f"{kayit['name']} - her {kayit['every_songs']} sarkida bir")
    else:
        jingle_status_yaz(last_added=f"{kayit['name']} - saatli {kayit['hour'] or 'her saat'}:{int(kayit['minute']):02d}")
    ayarlari_yaz(ayarlar)
    playlist_yaz(ayarlar.get("active_playlist", "all"), ayarlar)
    liquidsoap_komut("music.reload")
    return redirect(url_for("jingle"))


@app.route("/jingle-duzenle/<playlist_key>", methods=["POST"])
def jingle_duzenle(playlist_key):
    if playlist_key == "all":
        return redirect(url_for("jingle"))
    playlist_path = guvenli_yol("muzik", playlist_key)
    if not os.path.isdir(playlist_path):
        return redirect(url_for("jingle"))
    ayarlar = ayarlari_oku()
    existing = ayarlar.setdefault("playlists", {}).get(playlist_key, {})
    playlist_cfg = jingle_grup_form_kaydi(playlist_key, existing)
    ayarlar["playlists"][playlist_key] = playlist_cfg
    if playlist_cfg.get("playback_order") == "sequential":
        ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = False
    else:
        ayarlar.setdefault("playlist_shuffle", {})[playlist_key] = True
    ayarlari_yaz(ayarlar)
    muzik_cache_temizle()
    aktif_playlisti_yenile(ayarlar)
    return redirect(url_for("jingle"))


@app.route("/jingle-sil/<jingle_id>", methods=["POST"])
def jingle_sil(jingle_id):
    ayarlar = ayarlari_oku()
    if jingle_id in ayarlar.get("playlists", {}) and jingle_playlist_mi(jingle_id, ayarlar["playlists"][jingle_id]):
        ayarlar["playlists"].pop(jingle_id, None)
        ayarlar.get("playlist_shuffle", {}).pop(jingle_id, None)
        if ayarlar.get("active_playlist") == jingle_id:
            ayarlar["active_playlist"] = "all"
        playlist_path = guvenli_yol("muzik", jingle_id)
        if os.path.isdir(playlist_path) and not os.listdir(playlist_path):
            shutil.rmtree(playlist_path)
        ayarlari_yaz(ayarlar)
        muzik_cache_temizle()
        aktif_playlisti_yenile(ayarlar)
        return redirect(url_for("jingle"))
    ayarlar["jingles"] = [j for j in ayarlar.get("jingles", []) if j.get("id") != jingle_id]
    bekleyen_jingleleri_temizle(jingle_id)
    ayarlari_yaz(ayarlar)
    playlist_yaz(ayarlar.get("active_playlist", "all"), ayarlar)
    liquidsoap_komut("music.reload")
    jingle_kuyruklarini_temizle()
    return redirect(url_for("jingle"))


@app.route("/jingle-toggle/<jingle_id>", methods=["POST"])
def jingle_toggle(jingle_id):
    ayarlar = ayarlari_oku()
    if jingle_id in ayarlar.get("playlists", {}) and jingle_playlist_mi(jingle_id, ayarlar["playlists"][jingle_id]):
        playlist = ayarlar["playlists"][jingle_id]
        playlist["active"] = not playlist.get("active", True)
        playlist["updated_at"] = dt.datetime.now(ISTANBUL_TZ).strftime("%d.%m.%Y %H:%M:%S")
        ayarlari_yaz(ayarlar)
        aktif_playlisti_yenile(ayarlar)
        return redirect(url_for("jingle"))
    yeni_aktif = True
    for jingle in ayarlar.get("jingles", []):
        if jingle.get("id") == jingle_id:
            jingle["active"] = not jingle.get("active", True)
            yeni_aktif = jingle["active"]
            break
    if not yeni_aktif:
        bekleyen_jingleleri_temizle(jingle_id)
    ayarlari_yaz(ayarlar)
    playlist_yaz(ayarlar.get("active_playlist", "all"), ayarlar)
    liquidsoap_komut("music.reload")
    if not yeni_aktif:
        jingle_kuyruklarini_temizle()
    return redirect(url_for("jingle"))


@app.route("/jingle-hemen", methods=["POST"])
def jingle_hemen():
    path = request.form.get("path", "")
    if path:
        ayarlar = ayarlari_oku()
        cut_policy = request.form.get("cut_policy", "cut")
        if dj_gecis_modu_aktif_mi(ayarlar):
            cut_policy = "over"
        jingle_kuyruga_at({"paths": [path], "cut_policy": cut_policy})
    return redirect(url_for("jingle"))


@app.route("/sonraki-sarki", methods=["POST"])
def sonraki_sarki():
    liquidsoap_komut("music.skip")
    return redirect(url_for("index"))


@app.route("/kutuphane")
def kutuphane():
    tip = request.args.get("tip", "muzik")
    folder = request.args.get("folder", "")
    ayarlar = ayarlari_oku()
    editor_scope = request.args.get("editor_scope", "all")
    active_playlist = ayarlar.get("active_playlist", "all")
    editor_playlist = active_playlist if editor_scope == "active" else "all"
    try:
        editor_page = max(1, int(request.args.get("editor_page", "1")))
    except ValueError:
        editor_page = 1
    editor_query = request.args.get("editor_q", "")
    return render_template(
        "kutuphane.html",
        tip=tip,
        folder=folder,
        folders=klasorler(tip),
        files=kutuphane_dosyalari(tip, folder),
        disk=disk_bilgisi(),
        playlist_editor=playlist_editor_dosyalari(editor_playlist, ayarlar, editor_page, 100, editor_query),
        editor_scope=editor_scope,
        active_playlist=active_playlist,
    )


@app.route("/klasor-olustur", methods=["POST"])
def klasor_olustur():
    tip = request.form.get("tip", "muzik")
    name = secure_filename(request.form.get("name", "").strip())
    if name:
        os.makedirs(guvenli_yol(tip, name), exist_ok=True)
    return redirect(url_for("kutuphane", tip=tip, folder=name))


@app.route("/dosya-yukle", methods=["POST"])
def dosya_yukle():
    tip = request.form.get("tip", "muzik")
    folder = request.form.get("folder", "")
    hedef_klasor = guvenli_yol(tip, folder)
    os.makedirs(hedef_klasor, exist_ok=True)
    for file in request.files.getlist("files"):
        if file and file.filename and ses_dosyasi_mi(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(hedef_klasor, filename))
    if tip == "muzik":
        muzik_cache_temizle()
        playlist_yaz(ayarlari_oku().get("active_playlist", "all"))
        liquidsoap_komut("music.reload")
    return redirect(url_for("kutuphane", tip=tip, folder=folder))


@app.route("/dosya-sil", methods=["POST"])
def dosya_sil():
    tip = request.form.get("tip", "muzik")
    rel = request.form.get("rel", "")
    hedef = guvenli_yol(tip, rel)
    if os.path.isfile(hedef):
        os.remove(hedef)
    if tip == "muzik":
        muzik_cache_temizle()
        playlist_yaz(ayarlari_oku().get("active_playlist", "all"))
        liquidsoap_komut("music.reload")
    return redirect(url_for("kutuphane", tip=tip, folder=os.path.dirname(rel).replace("\\", "/")))


@app.route("/dosya-tasi", methods=["POST"])
def dosya_tasi():
    tip = request.form.get("tip", "muzik")
    rel = request.form.get("rel", "")
    folder = request.form.get("target_folder", "")
    kaynak = guvenli_yol(tip, rel)
    hedef_klasor = guvenli_yol(tip, folder)
    os.makedirs(hedef_klasor, exist_ok=True)
    if os.path.isfile(kaynak):
        shutil.move(kaynak, os.path.join(hedef_klasor, os.path.basename(kaynak)))
    if tip == "muzik":
        muzik_cache_temizle()
        playlist_yaz(ayarlari_oku().get("active_playlist", "all"))
        liquidsoap_komut("music.reload")
    return redirect(url_for("kutuphane", tip=tip, folder=folder))


@app.route("/media/<tip>/<path:rel>")
def media(tip, rel):
    hedef = guvenli_yol(tip, rel)
    if not os.path.isfile(hedef):
        return Response("Bulunamadi", status=404)
    return send_file(hedef, conditional=True)


@app.route("/api/now-playing")
def api_now_playing():
    now = aktif_metadata()
    ayarlar = ayarlari_oku()
    response = jsonify({
        "now": now,
        "next": siradaki_sarki_bilgisi(now),
        "planned_next": siradaki_planli_playlist(ayarlar),
        "history": gecmis_oku()[:PANEL_HISTORY_LIMIT],
        "status": container_durumu(),
    })
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response


@app.route("/api/health")
def api_health():
    interruption = yayin_kesinti_ozeti()
    def public_action(item):
        return {
            "time": item.get("time", ""),
            "action": item.get("action", ""),
            "success": item.get("success", ""),
            "reason": item.get("reason", ""),
        } if item else {}
    response = jsonify({
        "health": yayin_sagligi(),
        "jingle": jingle_ozeti(),
        "interruption": {
            "stops": interruption.get("stops", 0),
            "blocked": interruption.get("blocked", 0),
            "last_action": public_action(interruption.get("last_action", {})),
            "last_stop": public_action(interruption.get("last_stop", {})),
        },
    })
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response


if __name__ == "__main__":
    os.makedirs(STATE_YOLU, exist_ok=True)
    if not os.path.exists(AYARLAR_YOLU):
        ayarlari_yaz(varsayilan_ayarlar())
    liquidsoap_yaz(ayarlari_oku())
    scheduler_baslat()
    monitor_baslat()
    app.run(host="0.0.0.0", port=5000)


