Files

5993 lines
260 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from typing import List, Dict, Optional, Any
import sys
import os
import concurrent.futures as cf
import customtkinter as ctk
from tkinter import filedialog, messagebox
import tkinter as tk
from PIL import Image
import threading
import webbrowser
import requests
import io
import json
import time
import appdirs
import tempfile
import platform
from zipfile import ZipFile
from py7zr import SevenZipFile
import tarfile
from shutil import move
from turkanime_api.objects import Anime, Bolum
from turkanime_api.bypass import fetch
from turkanime_api.cli.dosyalar import Dosyalar
from turkanime_api.cli.cli_tools import VidSearchCLI, indir_aria2c
from turkanime_api.cli.gereksinimler import Gereksinimler
from turkanime_api.sources.animecix import CixAnime, search_animecix
from turkanime_api.sources.anizle import AnizleAnime, search_anizle, get_episode_streams
from turkanime_api.sources.tranime import (
TRAnimeAnime, TRAnimeEpisode, search_tranime,
get_anime_by_slug as get_tranime_anime,
get_anime_episodes as get_tranime_episodes,
get_episode_details as get_tranime_episode_details,
set_session_cookie as set_tranime_cookie
)
from turkanime_api.sources.adapter import AdapterAnime, AdapterBolum
from turkanime_api.anilist_client import anilist_client, AniListAuthServer
from turkanime_api.gui.update_manager import UpdateManager
from turkanime_api.common.utils import get_platform, get_arch
from turkanime_api.common.ui_helpers import create_progress_section
from turkanime_api.common.db import APIManager
from turkanime_api.common.adapters import SearchEngine
from turkanime_api.gui.cookie_browser import CookieBrowserWorker, is_available as cookie_browser_available
# Jikan API (MyAnimeList) importları
try:
from turkanime_api.jikan_client import (
jikan_client, get_seasonal_anime_list, get_trending_anime_list,
search_anime as jikan_search_anime, JIKAN_AVAILABLE
)
except ImportError:
JIKAN_AVAILABLE = False
jikan_search_anime = None # type: ignore[assignment]
try:
from turkanime_api.title_matching import get_title_matching_client, get_user_tracking_client
TITLE_MATCHING_AVAILABLE = True
except ImportError:
TITLE_MATCHING_AVAILABLE = False
# SearchEngine instance oluştur
search_engine = SearchEngine()
try:
from pypresence.presence import Presence
DISCORD_RPC_AVAILABLE = True
except ImportError:
DISCORD_RPC_AVAILABLE = False
class RequirementsManager:
"""GUI için gereksinimler yönetim sistemi."""
def __init__(self, parent_window):
self.parent = parent_window
self.dosyalar = Dosyalar()
self.tmp_dir = tempfile.TemporaryDirectory()
self.requirements_url = "https://raw.githubusercontent.com/KebabLord/turkanime-indirici/master/gereksinimler.json"
self.platform = get_platform()
self.arch = get_arch()
if self.platform == 'windows':
self.required_deps = ["yt-dlp", "mpv", "aria2c", "ffmpeg"]
else:
self.required_deps = []
def _get_embedded_tool_path(self, tool_name):
"""Embed edilmiş aracın yolunu döndür."""
try:
# PyInstaller ile gömülü ise
base_path = getattr(sys, "_MEIPASS", None)
if base_path:
tool_path = os.path.join(base_path, "bin", tool_name)
if os.path.exists(tool_path):
return tool_path
# Geliştirme ortamında
tool_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "bin", tool_name)
if os.path.exists(tool_path):
return tool_path
return None
except:
return None
def check_requirements(self):
"""Gereksinimleri kontrol et ve eksik olanları döndür."""
missing = []
for dep in self.required_deps:
if not self._is_app_available(dep):
missing.append(dep)
return missing
def _is_app_available(self, app_name):
"""Uygulamanın mevcut olup olmadığını kontrol et."""
try:
import subprocess
result = subprocess.run([app_name, "--version"],
capture_output=True, text=True, timeout=5)
return result.returncode == 0
except:
return False
def download_requirements(self, missing_deps, progress_callback=None):
"""Eksik gereksinimleri indir ve kur."""
try:
# Gereksinimler listesini al
response = requests.get(self.requirements_url, timeout=10)
response.raise_for_status()
requirements = response.json()
results = []
for dep in missing_deps:
if progress_callback:
progress_callback(f"{dep} indiriliyor...")
# Bu gereksinim için uygun URL'yi bul
req_data = None
for req in requirements:
if req["name"] == dep:
req_data = req
break
if not req_data:
results.append({"name": dep, "success": False, "error": "Gereksinim bulunamadı"})
continue
# Platform ve mimariye göre URL seç
platforms = req_data.get("platforms", {})
platform_data = platforms.get(self.platform, {})
url = platform_data.get(self.arch, platform_data.get("x64", ""))
if not url:
results.append({"name": dep, "success": False, "error": "Platform desteklenmiyor"})
continue
# Dosyayı indir
success, error = self._download_and_install(url, req_data, progress_callback)
results.append({"name": dep, "success": success, "error": error})
return results
except Exception as e:
return [{"name": "Genel", "success": False, "error": str(e)}]
def _download_and_install(self, url, req_data, progress_callback):
"""Dosyayı indir ve kur."""
try:
if progress_callback:
progress_callback(f"Dosya indiriliyor: {url.split('/')[-1]}")
# Özel durum: mpv placeholder ise gerçek mpv'yi indir
if req_data["name"] == "mpv":
mpv_path = self._get_embedded_tool_path("mpv.exe")
if mpv_path and os.path.exists(mpv_path):
with open(mpv_path, 'r', encoding='utf-8', errors='ignore') as f:
first_line = f.readline().strip()
if first_line.startswith("#"):
# Placeholder, gerçek mpv'yi indir
return self._download_real_mpv(url, req_data, progress_callback)
# Normal indirme işlemi
response = requests.get(url, stream=True, timeout=30)
response.raise_for_status()
filename = url.split("/")[-1]
filepath = os.path.join(self.tmp_dir.name, filename)
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
# Dosyayı kur
return self._install_file(filepath, req_data, progress_callback)
except Exception as e:
return False, str(e)
def _download_real_mpv(self, url, req_data, progress_callback):
"""mpv için gerçek indirme işlemi (placeholder yerine)."""
try:
if progress_callback:
progress_callback("Gerçek mpv indiriliyor...")
# mpv için daha güvenilir bir kaynak kullan
if self.platform == "windows":
# Windows için portable mpv indir
mpv_url = "https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20231231/mpv-x86_64-20231231-git-abc2a74.7z"
filename = "mpv.7z"
else:
# Diğer platformlar için normal URL kullan
mpv_url = url
filename = url.split("/")[-1]
response = requests.get(mpv_url, stream=True, timeout=30)
response.raise_for_status()
filepath = os.path.join(self.tmp_dir.name, filename)
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
# mpv'yi özel olarak kur
return self._install_mpv_file(filepath, req_data, progress_callback)
except Exception as e:
return False, str(e)
def _install_mpv_file(self, filepath, req_data, progress_callback):
"""mpv dosyasını özel olarak kur."""
try:
if progress_callback:
progress_callback("mpv kuruluyor...")
filename = os.path.basename(filepath)
file_ext = filename.split(".")[-1].lower()
if self.platform == "windows":
# Windows için 7z çıkarma
if file_ext == "7z":
import subprocess
# 7z ile çıkar
result = subprocess.run([
"C:\\Program Files\\7-Zip\\7z.exe", "x", filepath,
f"-o{self.tmp_dir.name}\\mpv_extracted", "-y"
], capture_output=True, text=True)
if result.returncode == 0:
# mpv.exe'yi bul ve bin klasörüne kopyala
extracted_dir = os.path.join(self.tmp_dir.name, "mpv_extracted")
for root, dirs, files in os.walk(extracted_dir):
for file in files:
if file.lower() == "mpv.exe":
src_path = os.path.join(root, file)
dest_path = self._get_embedded_tool_path("mpv.exe")
if dest_path:
import shutil
shutil.copy2(src_path, dest_path)
return True, None
return False, "mpv.exe extracted folder'da bulunamadı"
else:
return False, f"7z extraction failed: {result.stderr}"
else:
return False, f"Unsupported file type for mpv: {file_ext}"
else:
# Diğer platformlar için normal kurulum
return self._install_file(filepath, req_data, progress_callback)
except Exception as e:
return False, str(e)
def _install_file(self, filepath, req_data, progress_callback):
"""İndirilen dosyayı kur."""
try:
filename = os.path.basename(filepath)
file_ext = filename.split(".")[-1].lower()
if progress_callback:
progress_callback(f"Dosya kuruluyor: {filename}")
# Geçici dizin oluştur
extract_dir = tempfile.mkdtemp()
# Dosyayı çıkar
if file_ext == "7z":
with SevenZipFile(filepath, mode='r') as archive:
archive.extractall(extract_dir)
elif file_ext == "zip":
with ZipFile(filepath, 'r') as archive:
archive.extractall(extract_dir)
elif file_ext in ["xz", "gz", "bz2"]:
with tarfile.open(filepath, 'r:*') as archive:
archive.extractall(extract_dir)
elif file_ext == "exe":
if req_data.get("is_setup", False):
# Setup dosyası - çalıştır
os.system(f'"{filepath}"')
return True, None
else:
# Direkt kopyala
dest_path = os.path.join(self.dosyalar.ta_path, filename)
move(filepath, dest_path)
return True, None
else:
return False, f"Desteklenmeyen dosya türü: {file_ext}"
# Uygulama dosyasını bul ve taşı
app_files = []
for root, dirs, files in os.walk(extract_dir):
for file in files:
if req_data["name"] in file.lower() or file.lower().startswith(req_data["name"]):
app_files.append(os.path.join(root, file))
if not app_files:
# İlk executable dosyayı kullan
for root, dirs, files in os.walk(extract_dir):
for file in files:
if file_ext in ["exe", ""] or not "." in file:
app_files.append(os.path.join(root, file))
break
if app_files:
break
if app_files:
dest_path = os.path.join(self.dosyalar.ta_path, os.path.basename(app_files[0]))
move(app_files[0], dest_path)
return True, None
else:
return False, "Uygulama dosyası bulunamadı"
except Exception as e:
return False, str(e)
class WorkerSignals:
"""CustomTkinter için sinyal benzeri sınıf."""
def __init__(self):
self.progress_callbacks = []
self.progress_item_callbacks = []
self.error_callbacks = []
self.error_item_callbacks = []
self.success_callbacks = []
self.found_callbacks = []
def connect_progress(self, callback):
self.progress_callbacks.append(callback)
def connect_progress_item(self, callback):
self.progress_item_callbacks.append(callback)
def connect_error(self, callback):
self.error_callbacks.append(callback)
def connect_error_item(self, callback):
self.error_item_callbacks.append(callback)
def connect_success(self, callback):
self.success_callbacks.append(callback)
def connect_found(self, callback):
self.found_callbacks.append(callback)
def emit_progress(self, msg):
for cb in self.progress_callbacks:
cb(msg)
def emit_progress_item(self, data):
for cb in self.progress_item_callbacks:
cb(data)
def emit_error(self, msg):
for cb in self.error_callbacks:
cb(msg)
def emit_error_item(self, data):
for cb in self.error_item_callbacks:
cb(data)
def emit_success(self):
for cb in self.success_callbacks:
cb()
def emit_found(self, data):
for cb in self.found_callbacks:
cb(data)
class DownloadWorker:
"""CustomTkinter için indirme worker'ı."""
def __init__(self, bolumler: List[Bolum], update_callback=None):
self.bolumler = bolumler
self.signals = WorkerSignals()
self.update_callback = update_callback
self._cancelled = False
def cancel(self):
"""İndirmeyi iptal et."""
self._cancelled = True
def run(self):
try:
dosya = Dosyalar()
paralel = dosya.ayarlar.get("paralel indirme sayisi", 2)
max_retries = 2 # Hata durumunda tekrar deneme sayısı
def dl_one(bolum: Bolum):
if self._cancelled:
return
self.signals.emit_progress(f"{bolum.slug} için video aranıyor…")
self.signals.emit_progress_item({
"slug": bolum.slug,
"title": bolum.title,
"status": "hazır",
"downloaded": 0,
"total": None,
"percent": 0,
"speed": None,
"eta": None,
})
best_video = bolum.best_video(
by_res=dosya.ayarlar.get("max resolution", True),
early_subset=dosya.ayarlar.get("1080p aday sayısı", 8),
)
if not best_video:
self.signals.emit_error_item({
"slug": bolum.slug,
"title": bolum.title,
"error": "Uygun video bulunamadı",
})
return
down_dir = dosya.ayarlar.get("indirilenler", ".")
last = {"t": None, "b": 0}
def hook(h):
if self._cancelled:
return
st = h.get("status")
if st == "error":
msg = h.get("message")
if msg:
self.signals.emit_progress(f"{bolum.slug}: hata: {msg}")
cur = h.get("downloaded_bytes") or h.get("downloaded")
tot = h.get("total_bytes") or h.get("total_bytes_estimate") or h.get("total")
eta = h.get("eta")
spd = h.get("speed")
try:
import time as _t
now = _t.time()
if cur is not None:
if last["t"] is not None:
dt = max(1e-3, now - last["t"])
db = max(0, cur - last["b"]) if last["b"] is not None else 0
if db > 0:
spd = db / dt
last["t"], last["b"] = now, cur
except Exception:
pass
pct = None
if cur and tot:
try:
pct = int(cur * 100 / tot)
except Exception:
pct = None
if st == "downloading":
if cur and tot:
self.signals.emit_progress(f"{bolum.slug}: {int(cur/1024/1024)}/{int(tot/1024/1024)} MB")
else:
self.signals.emit_progress(f"{bolum.slug}: indiriliyor…")
elif st == "finished":
self.signals.emit_progress(f"{bolum.slug}: indirildi")
self.signals.emit_progress_item({
"slug": bolum.slug,
"title": bolum.title,
"status": ("indiriliyor" if st == "downloading" else "indirildi" if st == "finished" else st),
"downloaded": cur,
"total": tot,
"percent": pct,
"speed": spd,
"eta": eta,
})
# İndirme denemesi (retry mekanizması)
success = False
last_error = None
for attempt in range(1, max_retries + 1):
if self._cancelled:
break
if attempt > 1:
self.signals.emit_progress(f"{bolum.slug}: {attempt}. deneme...")
self.signals.emit_progress_item({
"slug": bolum.slug,
"title": bolum.title,
"status": f"tekrar ({attempt}/{max_retries})",
"downloaded": 0,
"total": None,
"percent": 0,
"speed": None,
"eta": None,
})
last["t"], last["b"] = None, 0
try:
if best_video.player != "ALUCARD(BETA)" and dosya.ayarlar.get("aria2c kullan"):
try:
success = bool(indir_aria2c(best_video, callback=hook, output=down_dir))
except Exception:
best_video.indir(callback=hook, output=down_dir)
success = True
else:
best_video.indir(callback=hook, output=down_dir)
success = True
except Exception as e:
last_error = str(e)
success = False
if success:
break
# Sonuç işleme
if success:
try:
dosya.set_gecmis(bolum.anime.slug if bolum.anime else "", bolum.slug, "indirildi")
except Exception:
pass
if self.update_callback:
try:
self.update_callback(bolum, down_dir)
except Exception:
pass
self.signals.emit_progress_item({
"slug": bolum.slug,
"title": bolum.title,
"status": ("tamamlandı" if success else "iptal" if self._cancelled else "hata"),
"downloaded": last.get("b"),
"total": last.get("b") if success else None,
"percent": (100 if success else None),
"speed": None,
"eta": 0,
"error": last_error if not success and not self._cancelled else None,
})
with cf.ThreadPoolExecutor(max_workers=paralel) as executor:
futures = [executor.submit(dl_one, b) for b in self.bolumler]
for fut in cf.as_completed(futures):
try:
fut.result()
except Exception:
pass
self.signals.emit_success()
except Exception as e:
self.signals.emit_error(str(e))
class VideoFindWorker:
"""Bölüm için en uygun videoyu bulur ve sonucu döndürür."""
def __init__(self, bolum: Bolum):
self.bolum = bolum
self.signals = WorkerSignals()
def run(self):
try:
dosya = Dosyalar()
vid_cli = VidSearchCLI()
best = self.bolum.best_video(
by_res=dosya.ayarlar.get("max resolution", True),
early_subset=dosya.ayarlar.get("1080p aday sayısı", 8),
callback=vid_cli.callback,
)
if not best:
self.signals.emit_error("Uygun video bulunamadı")
return
self.signals.emit_progress("Video bulundu")
self.signals.emit_success()
self.signals.emit_found(best)
except Exception as e:
self.signals.emit_error(str(e))
class SearchWorker:
"""Kaynağa göre arama yapan worker."""
def __init__(self, source: str, query: str):
self.source = source
self.query = query
self.signals = WorkerSignals()
def run(self):
try:
results = []
q = (self.query or "").strip()
if not q:
self.signals.emit_found([])
return
if self.source == "TürkAnime":
all_list = Anime.get_anime_listesi()
for slug, name in all_list:
if q.lower() in (name or "").lower():
results.append({"source": "TürkAnime", "slug": slug, "title": name})
else:
for _id, name in search_animecix(q):
# _id'yi güvenli şekilde int'e çevir
try:
safe_id = int(_id)
except (ValueError, TypeError):
safe_id = hash(str(_id)) % 1000000
results.append({"source": "AnimeciX", "id": safe_id, "title": name})
self.signals.emit_found(results)
except Exception as e:
self.signals.emit_error(str(e))
class EpisodesWorker:
"""Seçilen anime için bölüm listesini yükler."""
def __init__(self, anime_item: dict):
self.anime_item = anime_item
self.signals = WorkerSignals()
def run(self):
try:
src = self.anime_item.get("source")
out_items = []
if src == "TürkAnime":
ani = Anime(self.anime_item.get("slug"))
bolumler = ani.bolumler
for b in bolumler:
out_items.append({"title": b.title, "obj": b})
else:
anime_id = self.anime_item.get("id")
anime_title = self.anime_item.get("title")
if anime_id and anime_title:
# anime_id'yi güvenli şekilde int'e çevir
try:
safe_anime_id = int(anime_id)
except (ValueError, TypeError):
# String ID ise hash değeri al
safe_anime_id = hash(str(anime_id)) % 1000000
cix = CixAnime(id=str(safe_anime_id), title=anime_title)
cix_eps = cix.episodes
ada = AdapterAnime(slug=str(cix.id), title=cix.title)
for e in cix_eps:
ab = AdapterBolum(url=e.url, title=e.title, anime=ada)
out_items.append({"title": e.title, "obj": ab})
self.signals.emit_found(out_items)
except Exception as e:
self.signals.emit_error(str(e))
def _resource_path(rel_path: str) -> str:
"""PyInstaller tek-dosya ve geliştirme ortamında kaynak yolu çözer.
- Çalışma zamanı (_MEIPASS) içinde: docs klasörü Analysis.datas ile köke kopyalanır.
boot.py ve spec, docs/TurkAnime.ico'yu datas'a ekliyor; bu yüzden _MEIPASS/docs/... bekleriz.
- Geliştirme sırasında: proje kökü altındaki göreli yol kullanılır.
"""
try:
base = getattr(sys, "_MEIPASS", None)
if base and os.path.isdir(base):
cand = os.path.join(base, rel_path)
if os.path.exists(cand):
return cand
except Exception:
pass
# Proje kökü: bu dosyanın 3 üstü
try:
root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
cand = os.path.join(root, rel_path)
if os.path.exists(cand):
return cand
except Exception:
pass
# Son çare: göreli yol
return rel_path
class MainWindow(ctk.CTk):
def __init__(self):
super().__init__()
self.title("TürkAnime Gui by @barkeser2002") # Kalsın
self.geometry("1400x900")
# App icon (Windows: .ico, Others: .png via iconphoto)
try:
ico_path = _resource_path(os.path.join('docs', 'Turkanime.ico'))
if os.path.exists(ico_path):
# On Windows, iconbitmap expects .ico and updates taskbar/title icon
self.iconbitmap(ico_path)
else:
png_path = _resource_path(os.path.join('docs', 'Turkanime.png'))
if os.path.exists(png_path):
self._app_icon_ref = tk.PhotoImage(file=png_path)
self.iconphoto(True, self._app_icon_ref)
except Exception:
pass
self.dosya = Dosyalar()
# Değişkenler
self.anilist_auth_server = None
self.anilist_user = None
self.anilist_current_list_type = "CURRENT" # CURRENT, PLANNING, COMPLETED, DROPPED, PAUSED
self.anilist_trending_cache = []
self.anilist_search_cache = {}
self.anilist_image_cache = {}
self.local_anime_progress = {}
self.current_view = "home" # home, search, trending, watchlist
self.selected_anime = None # Seçili anime için
self.selected_source = "AnimeciX" # Varsayılan kaynak
self.downloaded_episodes = [] # İndirilen bölümler listesi
# Episodes listesi için değişkenler
self.episodes_vars = [] # [(var, obj)]
self.episodes_objs = []
self.episodes_list = None
self.source_accordion: Optional[Any] = None
# İndirme sistemi değişkenleri
self._active_download_worker = None
self._download_panel = {}
self._download_frame = None
# Discord Rich Presence değişkenleri
self.discord_rpc = None
self.discord_connected = False
self.discord_update_timer = None
self._resize_timer = None
self.user_tooltip = None
# Manager değişkenleri
self.requirements_manager: Optional[RequirementsManager] = None
self.update_manager: Optional[UpdateManager] = None
# Performans iyileştirmesi: UI'yi adım adım yükle
self._init_ui_async()
def _init_ui_async(self):
"""UI'yi adım adım yükleyerek performans iyileştirmesi."""
# İlk adım: Temel yapıyı oluştur
self._create_basic_structure()
# İkinci adım: Ana içeriği oluştur (50ms sonra)
self.after(50, self._create_main_content)
# Üçüncü adım: Gereksinimler ve güncellemeleri kontrol et (100ms sonra)
self.after(100, self._create_requirements_and_updates)
# Dördüncü adım: Discord RPC'yi başlat (150ms sonra)
self.after(150, self._init_discord_async)
def _create_basic_structure(self):
"""Temel UI yapısını oluştur."""
# Ana container
self.main_container = ctk.CTkFrame(self, fg_color="#0f0f0f")
self.main_container.pack(fill="both", expand=True)
# Header/Navigation
self.create_header()
# Ana içerik alanı
self.content_area = ctk.CTkScrollableFrame(self.main_container, fg_color="transparent")
self.content_area.pack(fill="both", expand=True, padx=20, pady=(0, 20))
# UI güncellemesi için update_idletasks kullan
self.update_idletasks()
def _create_main_content(self):
"""Ana içeriği oluştur."""
# Ana sayfa içeriği
self.create_home_content()
# Alt bar
self.create_bottom_bar()
# UI güncellemesi için update_idletasks kullan
self.update_idletasks()
def _create_requirements_and_updates(self):
"""Gereksinimler ve güncellemeleri thread ile başlat."""
# TRAnimeİzle cookie'yi ayarlardan yükle
try:
dosya = Dosyalar()
tranime_cookie = dosya.ayarlar.get("tranime_cookie", "")
if tranime_cookie:
set_tranime_cookie(tranime_cookie)
with open("debug.log", "a") as f:
f.write("INFO: TRAnimeİzle cookie yüklendi\n")
else:
# Cookie boşsa ve daha önce sorulmadıysa otomatik toplama teklif et
prompted = dosya.ayarlar.get("cookie_browser_prompted", False)
if not prompted:
self.after(3000, self._prompt_cookie_browser)
except Exception as e:
with open("debug.log", "a") as f:
f.write(f"ERROR: TRAnimeİzle cookie yükleme hatası: {e}\n")
# Gereksinimler kontrolü - thread ile
def init_requirements():
try:
self.requirements_manager = RequirementsManager(self)
self.update_manager = UpdateManager(self, dosyalar=self.dosya)
# Ana thread'de çalıştır
self.after(0, self.check_requirements_on_startup)
# Otomatik güncelleme kontrolü
self.after(2000, self.check_for_updates_silent)
except Exception as e:
print(f"Gereksinimler başlatma hatası: {e}")
# Thread ile başlat
threading.Thread(target=init_requirements, daemon=True).start()
# UI güncellemesi için update_idletasks kullan
self.update_idletasks()
def _init_discord_async(self):
"""Discord RPC'yi başlat."""
# Discord Rich Presence'i başlat - thread ile
def init_discord():
try:
# Ana thread'de çalıştır
self.after(0, self.init_discord_rpc)
except Exception as e:
print(f"Discord RPC başlatma hatası: {e}")
# Thread ile başlat
threading.Thread(target=init_discord, daemon=True).start()
# UI güncellemesi için update_idletasks kullan
self.update_idletasks()
def init_discord_rpc(self):
"""Discord Rich Presence'i başlat."""
# Ayarlardan Discord Rich Presence'in açık olup olmadığını kontrol et
dosya = Dosyalar()
if not dosya.ayarlar.get("discord_rich_presence", True):
self.discord_connected = False
return
try:
self.discord_rpc = Presence("1115609536552771595") # Application ID
self.discord_rpc.connect()
self.discord_connected = True
# Başlangıç durumu
self.update_discord_presence("Ana sayfada", "TürkAnime GUI")
# Periyodik güncelleme için timer başlat
self.discord_update_timer = self.after(15000, self.update_discord_presence_periodic)
# Buton rengini güncelle
self.update_discord_button_color()
except Exception as e:
print(f"Discord RPC bağlantı hatası: {e}")
self.discord_connected = False
self.discord_rpc = None
def update_discord_presence(self, details, state, large_image="Turkanime", small_image=None,
large_text="TürkAnime", small_text=None, start_time=None, buttons=None):
"""Discord Rich Presence'i güncelle."""
if not self.discord_connected or not self.discord_rpc:
return
try:
presence_data = {
"details": details,
"state": state,
"large_image": large_image,
"large_text": large_text
}
if small_image:
presence_data["small_image"] = small_image
if small_text:
presence_data["small_text"] = small_text
if start_time:
presence_data["start"] = start_time
# Button desteği ekle
if buttons:
presence_data["buttons"] = buttons
else:
# Varsayılan button - uygulamayı edin
presence_data["buttons"] = [
{
"label": "Uygulamayı Edin",
"url": "https://github.com/barkeser2002/turkanime-gui/releases"
}
]
self.discord_rpc.update(**presence_data)
except Exception as e:
print(f"Discord RPC güncelleme hatası: {e}")
# Bağlantı kaybı durumunda yeniden bağlanmayı dene
self.discord_connected = False
self.attempt_reconnect_discord()
def update_discord_presence_periodic(self):
"""Periyodik Discord Rich Presence güncellemesi."""
if not self.discord_connected:
return
try:
# Mevcut view'a göre durumu güncelle
if hasattr(self, 'current_view'):
if self.current_view == "home":
self.update_discord_presence("Ana sayfada", "TürkAnime GUI")
elif self.current_view == "trending":
self.update_discord_presence("Trend animelere bakıyor", "TürkAnime GUI")
elif self.current_view == "downloads":
self.update_discord_presence("İndirilenlere bakıyor", "TürkAnime GUI")
elif self.current_view == "watchlist":
self.update_discord_presence("İzleme listesine bakıyor", "TürkAnime GUI")
else:
self.update_discord_presence("Anime arıyor", "TürkAnime GUI")
# 15 saniye sonra tekrar güncelle
self.discord_update_timer = self.after(15000, self.update_discord_presence_periodic)
except Exception as e:
print(f"Discord RPC periyodik güncelleme hatası: {e}")
self.discord_connected = False
self.attempt_reconnect_discord()
def update_discord_presence_anime(self, anime_title, episode_info=None, anime_image=None, buttons=None):
"""Anime izlerken Discord Rich Presence'i güncelle."""
if not self.discord_connected:
return
try:
details = f"{anime_title} izliyor"
state = episode_info if episode_info else "Anime izliyor"
# Anime resmi varsa kullan, yoksa default
large_image = "Turkanime" # default
large_text = anime_title
# Anime resmi varsa kullan (URL'yi Discord'a uygun formata çevir)
if anime_image:
# Discord Rich Presence için resmi base64'e çevir veya URL kullan
try:
# Basit URL kontrolü
if anime_image.startswith('http'):
# Discord Rich Presence için resmi küçültülmüş versiyonunu kullan
# AniList'in large resmi çok büyük olabilir, medium kullan
if 'large' in anime_image:
anime_image = anime_image.replace('large', 'medium')
large_image = anime_image
large_text = f"{anime_title} - {episode_info}" if episode_info else anime_title
except:
pass
# Başlangıç zamanını ekle
import time
start_time = time.time()
self.update_discord_presence(details, state, large_image=large_image,
large_text=large_text, start_time=start_time, buttons=buttons)
except Exception as e:
print(f"Discord RPC anime güncelleme hatası: {e}")
# Hata durumunda basit güncelleme dene
try:
self.update_discord_presence(f"{anime_title} izliyor",
episode_info if episode_info else "Anime izliyor")
except Exception:
pass
def attempt_reconnect_discord(self):
"""Discord Rich Presence bağlantısını yeniden kurmayı dene."""
if not DISCORD_RPC_AVAILABLE:
return
# Ayarlardan Discord Rich Presence'in açık olup olmadığını kontrol et
dosya = Dosyalar()
if not dosya.ayarlar.get("discord_rich_presence", True):
return
try:
print("Discord RPC yeniden bağlanmaya çalışılıyor...")
self.discord_rpc = Presence("1115609536552771595")
self.discord_rpc.connect()
self.discord_connected = True
print("Discord RPC yeniden bağlandı")
# Mevcut durumu güncelle
self.update_discord_presence("TürkAnime'ya geri döndü", "TürkAnime GUI")
# Periyodik güncellemeyi yeniden başlat
if hasattr(self, 'discord_update_timer') and self.discord_update_timer:
self.after_cancel(self.discord_update_timer)
self.discord_update_timer = self.after(15000, self.update_discord_presence_periodic)
# Buton rengini güncelle
self.update_discord_button_color()
except Exception as e:
print(f"Discord RPC yeniden bağlanma hatası: {e}")
self.discord_connected = False
# 30 saniye sonra tekrar dene
self.after(30000, self.attempt_reconnect_discord)
def disconnect_discord_rpc(self):
"""Discord Rich Presence bağlantısını kapat."""
try:
if self.discord_rpc:
self.discord_rpc.clear()
self.discord_rpc.close()
self.discord_rpc = None
self.discord_connected = False
# Timer'ı iptal et
if hasattr(self, 'discord_update_timer') and self.discord_update_timer:
self.after_cancel(self.discord_update_timer)
except Exception as e:
print(f"Discord RPC kapatma hatası: {e}")
# Buton rengini güncelle
self.update_discord_button_color()
def update_discord_button_color(self):
"""Discord butonunun rengini bağlantı durumuna göre güncelle."""
# Discord test butonu kaldırıldı, bu fonksiyon artık kullanılmıyor
pass
def update_discord_presence_download(self, anime_title, progress=None):
"""İndirme sırasında Discord Rich Presence'i güncelle."""
if not self.discord_connected:
return
try:
details = f"{anime_title} indiriyor"
state = f"İlerleme: {progress}%" if progress else "Anime indiriyor"
self.update_discord_presence(details, state)
except Exception as e:
print(f"Discord RPC indirme güncelleme hatası: {e}")
def create_header(self):
"""Modern header oluştur."""
header_frame = ctk.CTkFrame(self.main_container, fg_color="#1a1a1a", height=60)
header_frame.pack(fill="x", padx=0, pady=0)
header_frame.pack_propagate(False)
# Logo ve başlık
logo_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
logo_frame.pack(side="left", padx=12, pady=8)
# Logo ikonu
try:
icon_path = _resource_path(os.path.join('docs', 'Turkanime.png'))
if os.path.exists(icon_path):
logo_image = ctk.CTkImage(Image.open(icon_path), size=(32, 32))
logo_label = ctk.CTkLabel(logo_frame, image=logo_image, text="")
logo_label.pack(side="left", padx=(0, 10))
except Exception:
pass
# Başlık
title_label = ctk.CTkLabel(logo_frame, text="TürkAnime",
font=ctk.CTkFont(size=22, weight="bold"),
text_color="#ff6b6b")
title_label.pack(side="left")
# Ana navigasyon - pill style
nav_frame = ctk.CTkFrame(header_frame, fg_color="#0f0f0f", corner_radius=20)
nav_frame.pack(side="left", padx=20, pady=10)
self.btnHome = ctk.CTkButton(nav_frame, text="🏠 Ana Sayfa", command=self.show_home,
fg_color="transparent", text_color="#ffffff",
font=ctk.CTkFont(size=11, weight="bold"),
hover_color="#333333", corner_radius=15,
width=90, height=30)
self.btnHome.pack(side="left", padx=3, pady=3)
self.btnSeason = ctk.CTkButton(nav_frame, text="📺 Bu Sezon", command=self.show_season,
fg_color="transparent", text_color="#cccccc",
font=ctk.CTkFont(size=11),
hover_color="#333333", corner_radius=15,
width=90, height=30)
self.btnSeason.pack(side="left", padx=3, pady=3)
self.btnTrending = ctk.CTkButton(nav_frame, text="🔥 Trend", command=self.show_trending,
fg_color="transparent", text_color="#cccccc",
font=ctk.CTkFont(size=11),
hover_color="#333333", corner_radius=15,
width=70, height=30)
self.btnTrending.pack(side="left", padx=3, pady=3)
self.btnDownloads = ctk.CTkButton(nav_frame, text="⬇️ İndirilenler", command=self.show_downloads,
fg_color="transparent", text_color="#cccccc",
font=ctk.CTkFont(size=11),
hover_color="#333333", corner_radius=15,
width=100, height=30)
self.btnDownloads.pack(side="left", padx=3, pady=3)
# Listem butonu - sadece giriş yapıldıysa göster
self.btnWatchlist = ctk.CTkButton(nav_frame, text="📋 Listem", command=self.show_watchlist,
fg_color="transparent", text_color="#cccccc",
font=ctk.CTkFont(size=11),
hover_color="#333333", corner_radius=15,
width=80, height=30)
# Başlangıçta gizli, check_anilist_auth_status'te kontrol edilecek
# Arama çubuğu - daha kompakt
search_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
search_frame.pack(side="left", padx=10, expand=True)
# Kaynak ve başlık seçimi birleşik frame
selectors_frame = ctk.CTkFrame(search_frame, fg_color="transparent")
selectors_frame.pack(side="left", padx=(0, 10))
self.cmbSource = ctk.CTkComboBox(selectors_frame, values=["TürkAnime", "AnimeciX", "Anizle", "TRAnimeİzle"],
width=110, height=30,
command=self.on_source_change,
font=ctk.CTkFont(size=10))
self.cmbSource.pack(side="left", padx=(0, 5))
self.cmbTitle = ctk.CTkComboBox(selectors_frame, values=["🇺🇸 İngilizce", "🇯🇵 Romanji"],
width=110, height=30,
command=self.on_title_change,
font=ctk.CTkFont(size=10))
self.cmbTitle.pack(side="left")
# Arama kutusu
self.searchEntry = ctk.CTkEntry(search_frame, placeholder_text="🔍 Anime ara...",
width=180, height=30,
font=ctk.CTkFont(size=11),
corner_radius=15)
self.searchEntry.pack(side="left", padx=(0, 5))
self.btnSearch = ctk.CTkButton(search_frame, text="Ara", width=50, height=30,
command=self.on_search,
fg_color="#ff6b6b", hover_color="#ff5252",
corner_radius=15, font=ctk.CTkFont(size=11))
self.btnSearch.pack(side="left")
# Sağ taraf - Kullanıcı alanı
user_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
user_frame.pack(side="right", padx=12)
# Ayarlar butonu
self.btnSettings = ctk.CTkButton(user_frame, text="⚙️", width=35, height=35,
command=self.on_open_settings,
fg_color="#333333", hover_color="#444444",
corner_radius=8)
self.btnSettings.pack(side="right", padx=(5, 0))
# AniList butonları
self.anilist_panel = ctk.CTkFrame(user_frame, fg_color="transparent")
self.anilist_panel.pack(side="right", padx=(0, 5))
self.btnAniListLogin = ctk.CTkButton(self.anilist_panel, text="AniList Giriş",
command=self.on_anilist_login,
fg_color="#02a9ff", hover_color="#0294de",
width=90, height=30, corner_radius=15,
font=ctk.CTkFont(size=10))
self.btnAniListLogin.pack(side="left", padx=(0, 5))
self.btnAniListLogout = ctk.CTkButton(self.anilist_panel, text="Çıkış",
command=self.on_anilist_logout,
fg_color="#666666", width=50, height=30,
corner_radius=15, font=ctk.CTkFont(size=10))
self.btnAniListLogout.pack(side="left", padx=(0, 5))
self.btnAniListLogout.pack_forget() # Başlangıçta gizli
# Kullanıcı adı label'ı
self.lblAniListUser = ctk.CTkLabel(self.anilist_panel, text="",
font=ctk.CTkFont(size=10),
text_color="#cccccc")
self.lblAniListUser.pack(side="left", padx=(0, 5))
# Avatar için image label
self.avatarLabel = ctk.CTkLabel(self.anilist_panel, text="", width=30, height=30)
self.avatarLabel.pack(side="left")
# Avatar'a hover efekti için
self.avatarLabel.bind("<Enter>", self.show_user_tooltip)
self.avatarLabel.bind("<Leave>", self.hide_user_tooltip)
# AniList panel toggle butonu
self.btnAniListToggle = ctk.CTkButton(
user_frame, text="👤 Gizle",
command=self.toggle_anilist_panel,
fg_color="#555555", hover_color="#666666",
width=80, height=30, corner_radius=15,
font=ctk.CTkFont(size=10))
self.btnAniListToggle.pack(side="right", padx=(0, 5))
self.anilist_visible = True
def show_season(self):
"""Bu sezon sayfası göster - LiveChart verisi."""
self.current_view = "season"
self.clear_content_area()
# Navigasyon butonlarını güncelle
self._update_nav_buttons("season")
# Discord Rich Presence güncelle
self.update_discord_presence("Bu sezon animelerine bakıyor", "TürkAnime GUI")
# Başlık
title_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
title_frame.pack(fill="x", pady=(20, 20))
back_btn = ctk.CTkButton(title_frame, text="← Ana Sayfa",
command=self.show_home,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"))
back_btn.pack(side="left")
# Sezon bilgisi
from datetime import datetime
now = datetime.now()
month = now.month
year = now.year
if month in [1, 2, 3]:
season_name = "Kış"
elif month in [4, 5, 6]:
season_name = "İlkbahar"
elif month in [7, 8, 9]:
season_name = "Yaz"
else:
season_name = "Sonbahar"
season_title = ctk.CTkLabel(title_frame, text=f"📺 {season_name} {year} Sezonu",
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
season_title.pack(side="left", padx=30)
season_desc = ctk.CTkLabel(title_frame, text="MyAnimeList verisi • Jikan API",
font=ctk.CTkFont(size=11),
text_color="#888888")
season_desc.pack(side="left")
# Sezon grid
self.season_grid = ctk.CTkFrame(self.content_area, fg_color="transparent")
self.season_grid.pack(fill="both", expand=True)
# Loading
loading_label = ctk.CTkLabel(self.season_grid, text="Bu sezon animeler yükleniyor...",
font=ctk.CTkFont(size=16),
text_color="#888888")
loading_label.pack(pady=50)
# Jikan API'den sezon animelerini yükle
def load_worker():
try:
if JIKAN_AVAILABLE:
seasonal = get_seasonal_anime_list()
if seasonal and len(seasonal) > 0:
self.after(0, lambda: self.display_season_anime(seasonal, loading_label))
return
# Fallback to AniList trending
seasonal = anilist_client.get_trending_anime()
self.after(0, lambda: self.display_season_anime(seasonal, loading_label))
except Exception as e:
error_msg = str(e)
self.after(0, lambda: self.show_error(error_msg, loading_label))
threading.Thread(target=load_worker, daemon=True).start()
def display_season_anime(self, anime_list, loading_label):
"""Sezon animelerini göster."""
loading_label.destroy()
# Anime listesini sakla (resize için)
self._season_anime_list = anime_list
# Grid'i oluştur
self._update_season_grid()
def _update_season_grid(self):
"""Season grid'i yeniden oluştur."""
if not hasattr(self, 'season_grid') or not self.season_grid.winfo_exists():
return
# Mevcut içeriği temizle
for widget in self.season_grid.winfo_children():
widget.destroy()
# Sütun sayısını hesapla
try:
available_width = self.season_grid.winfo_width()
if available_width < 100:
available_width = 1200
except:
available_width = 1200
max_cols = self._calculate_columns(available_width)
row = 0
col = 0
for anime in self._season_anime_list:
if col >= max_cols:
col = 0
row += 1
self.create_anime_card(self.season_grid, anime, row, col, max_cols)
col += 1
def _update_nav_buttons(self, active: str):
"""Navigasyon butonlarını güncelle."""
buttons = {
'home': self.btnHome,
'season': self.btnSeason if hasattr(self, 'btnSeason') else None,
'trending': self.btnTrending,
'downloads': self.btnDownloads,
'watchlist': self.btnWatchlist if hasattr(self, 'btnWatchlist') else None
}
for name, btn in buttons.items():
if btn:
if name == active:
btn.configure(text_color="#ffffff", font=ctk.CTkFont(size=11, weight="bold"))
else:
btn.configure(text_color="#cccccc", font=ctk.CTkFont(size=11))
def create_home_content(self):
"""Ana sayfa içeriği oluştur."""
# Hero section
self.create_hero_section()
# Trend animeler bölümü
self.create_trending_section()
# Kategoriler
self.create_categories_section()
def create_hero_section(self):
"""Gelişmiş hero section oluştur - Netflix tarzı."""
hero_frame = ctk.CTkFrame(self.content_area, fg_color="#1a1a1a", height=350,
corner_radius=15)
hero_frame.pack(fill="x", pady=(0, 40))
hero_frame.pack_propagate(False)
# Gradient efekti için frame
hero_content = ctk.CTkFrame(hero_frame, fg_color="transparent")
hero_content.pack(fill="both", expand=True, padx=50, pady=30)
# Sol taraf - Metin içeriği
left_frame = ctk.CTkFrame(hero_content, fg_color="transparent")
left_frame.pack(side="left", fill="both", expand=True)
# Ana başlık
title = ctk.CTkLabel(left_frame, text="🌟 Anime Dünyasını\nKeşfet",
font=ctk.CTkFont(size=42, weight="bold"),
text_color="#ffffff")
title.pack(anchor="w", pady=(20, 15))
# Alt başlık
subtitle = ctk.CTkLabel(left_frame,
text="Binlerce anime arasından favorilerinizi bulun,\nizleme listenizi yönetin ve toplulukla paylaşın.\nAniList entegrasyonu ile keşiflerinizi kişiselleştirin.",
font=ctk.CTkFont(size=16),
text_color="#cccccc",
wraplength=550)
subtitle.pack(anchor="w", pady=(0, 35))
# İstatistikler
stats_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
stats_frame.pack(anchor="w", pady=(0, 30))
stats = [
("🎬", "10,000+", "Anime"),
("👥", "2M+", "Kullanıcı"),
("⭐", "4.5/5", "Ortalama Skor")
]
for emoji, number, label in stats:
stat_frame = ctk.CTkFrame(stats_frame, fg_color="#2a2a2a", width=100, height=60,
corner_radius=8)
stat_frame.pack(side="left", padx=(0, 15))
stat_frame.pack_propagate(False)
stat_emoji = ctk.CTkLabel(stat_frame, text=emoji, font=ctk.CTkFont(size=16))
stat_emoji.pack(pady=(5, 0))
stat_number = ctk.CTkLabel(stat_frame, text=number,
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ff6b6b")
stat_number.pack()
stat_label = ctk.CTkLabel(stat_frame, text=label,
font=ctk.CTkFont(size=9),
text_color="#888888")
stat_label.pack()
# Aksiyon butonları
buttons_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
buttons_frame.pack(anchor="w")
self.btnExplore = ctk.CTkButton(buttons_frame, text="🔥 Trendleri Keşfet",
command=self.show_trending,
fg_color="#ff6b6b", hover_color="#ff5252",
width=180, height=50,
font=ctk.CTkFont(size=15, weight="bold"),
corner_radius=25)
self.btnExplore.pack(side="left", padx=(0, 20))
self.btnRandom = ctk.CTkButton(buttons_frame, text="🎲 Rastgele Anime",
command=self.show_random_anime,
fg_color="transparent", border_width=2,
border_color="#4ecdc4", text_color="#4ecdc4",
width=180, height=50,
font=ctk.CTkFont(size=15, weight="bold"),
corner_radius=25)
self.btnRandom.pack(side="left")
# Sağ taraf - Büyük görsel
right_frame = ctk.CTkFrame(hero_content, fg_color="transparent")
right_frame.pack(side="right", fill="both", expand=True)
# Ana görsel için büyük alan
hero_image_frame = ctk.CTkFrame(right_frame, fg_color="transparent", corner_radius=12)
hero_image_frame.pack(expand=True)
hero_image_frame.pack_propagate(False)
# Hero görselini yükle
try:
icon_path = _resource_path(os.path.join('docs', 'Turkanime.png'))
if os.path.exists(icon_path):
# Open image
pil_image = Image.open(icon_path)
# Set frame size to 300x300
hero_image_frame.configure(width=300, height=300)
# Create CTkImage with size 300x300
hero_image = ctk.CTkImage(pil_image, size=(300, 300))
hero_label = ctk.CTkLabel(hero_image_frame, image=hero_image, text="")
hero_label.pack(expand=True)
else:
# Fallback to text if icon not found
hero_label = ctk.CTkLabel(hero_image_frame, text="🎬\nTÜRK\nANİMU",
font=ctk.CTkFont(size=48, weight="bold"),
text_color="#ff6b6b")
hero_label.pack(expand=True)
except Exception:
# Fallback to text on error
hero_label = ctk.CTkLabel(hero_image_frame, text="🎬\nTÜRK\nANİMU",
font=ctk.CTkFont(size=48, weight="bold"),
text_color="#ff6b6b")
hero_label.pack(expand=True)
def show_random_anime(self):
"""Rastgele anime göster."""
self.message("Rastgele anime özelliği yakında eklenecek!")
def create_trending_section(self):
"""Trend animeler bölümü oluştur."""
section_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
section_frame.pack(fill="x", pady=(0, 40))
# Başlık ve açıklama
title_frame = ctk.CTkFrame(section_frame, fg_color="transparent")
title_frame.pack(fill="x", pady=(0, 25))
section_title = ctk.CTkLabel(title_frame, text="🔥 Bu Hafta Trend",
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
section_title.pack(side="left")
# Trend açıklama
trend_desc = ctk.CTkLabel(title_frame, text="AniList topluluğunun en sevdiği animeler",
font=ctk.CTkFont(size=12),
text_color="#888888")
trend_desc.pack(side="left", padx=(15, 0))
self.btnViewAllTrending = ctk.CTkButton(title_frame, text="Tümünü Gör →",
command=self.show_trending,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"),
hover_color="#ff5252")
self.btnViewAllTrending.pack(side="right")
# Trend animeler grid'i
self.trending_grid = ctk.CTkFrame(section_frame, fg_color="transparent")
self.trending_grid.pack(fill="both", expand=True, pady=(0, 12))
# Loading state
self.loading_label = ctk.CTkLabel(self.trending_grid, text="Trend animeler yükleniyor...",
font=ctk.CTkFont(size=14),
text_color="#888888")
self.loading_label.pack(pady=50)
# Trend animeleri yükle
self.load_trending_anime()
def create_categories_section(self):
"""Gelişmiş kategoriler bölümü oluştur."""
section_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
section_frame.pack(fill="x", pady=(0, 40))
# Başlık
section_title = ctk.CTkLabel(section_frame, text="🎯 Popüler Kategoriler",
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
section_title.pack(anchor="w", pady=(0, 25))
# Kategori butonları - daha büyük ve şık
categories_frame = ctk.CTkFrame(section_frame, fg_color="transparent")
categories_frame.pack(fill="x")
categories = [
("Aksiyon", "⚔️", "#ff6b6b", "Savaş ve macera dolu animeler"),
("Romantik", "💕", "#ff9ff3", "Aşk ve duygusal hikayeler"),
("Komedi", "😂", "#ffd93d", "Gülmece ve eğlence"),
("Fantastik", "🧙", "#a29bfe", "Büyü ve doğaüstü"),
("Bilim Kurgu", "🚀", "#74b9ff", "Gelecek ve teknoloji"),
("Dram", "🎭", "#fd79a8", "Duygusal ve derin hikayeler"),
("Mystery", "🔍", "#6c5ce7", "Gizem ve gerilim"),
("Horror", "👻", "#2d3436", "Korku ve dehşet")
]
for i, (name, emoji, color, desc) in enumerate(categories):
# Daha büyük kategori kartı
cat_frame = ctk.CTkFrame(categories_frame, fg_color=color, width=140, height=100,
corner_radius=12)
cat_frame.pack(side="left", padx=12, pady=8)
cat_frame.pack_propagate(False)
# Hover efekti
def on_enter(e, frame=cat_frame):
frame.configure(fg_color=self.lighten_color(color))
def on_leave(e, frame=cat_frame):
frame.configure(fg_color=color)
cat_frame.bind("<Enter>", on_enter)
cat_frame.bind("<Leave>", on_leave)
# Emoji
cat_emoji = ctk.CTkLabel(cat_frame, text=emoji, font=ctk.CTkFont(size=24))
cat_emoji.pack(pady=(8, 0))
# İsim
cat_label = ctk.CTkLabel(cat_frame, text=name,
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff")
cat_label.pack(pady=(2, 0))
# Kısa açıklama
cat_desc = ctk.CTkLabel(cat_frame, text=desc,
font=ctk.CTkFont(size=8),
text_color="#ffffff")
cat_desc.pack(pady=(0, 8))
def lighten_color(self, color):
"""Renk tonunu aç."""
# Basit renk açma fonksiyonu
if color == "#ff6b6b":
return "#ff8a8a"
elif color == "#ff9ff3":
return "#ffb3f7"
elif color == "#ffd93d":
return "#ffe066"
elif color == "#a29bfe":
return "#b8bffe"
elif color == "#74b9ff":
return "#8fc1ff"
elif color == "#fd79a8":
return "#ff91b8"
elif color == "#6c5ce7":
return "#857ce8"
elif color == "#2d3436":
return "#454e50"
else:
return color
def create_bottom_bar(self):
"""Alt bar oluştur."""
bottom_frame = ctk.CTkFrame(self.main_container, fg_color="#1a1a1a", height=60)
bottom_frame.pack(fill="x", side="bottom", padx=0, pady=0)
bottom_frame.pack_propagate(False)
# Sol taraf - Durum
status_frame = ctk.CTkFrame(bottom_frame, fg_color="transparent")
status_frame.pack(side="left", padx=20)
self.status_label = ctk.CTkLabel(status_frame, text="TürkAnime hazır",
font=ctk.CTkFont(size=12),
text_color="#cccccc")
self.status_label.pack()
def check_requirements_on_startup(self):
"""Eksik gereksinimleri kontrol et ve kullanıcıya bildir."""
# Embed edilmiş araçlar kullanılıyor, kontrolü atla
self.message("Embed edilmiş araçlar kullanılıyor", error=False)
def show_requirements_dialog(self, missing_deps):
"""Eksik gereksinimler için dialog göster."""
dialog = ctk.CTkToplevel(self)
dialog.title("Gereksinimler Eksik")
dialog.geometry("500x400")
dialog.transient(self)
dialog.grab_set()
# Başlık
title_label = ctk.CTkLabel(dialog, text="⚠️ Eksik Gereksinimler",
font=ctk.CTkFont(size=18, weight="bold"))
title_label.pack(pady=(20, 10))
# Açıklama
desc_text = f"Aşağıdaki gereksinimler bulunamadı:\n\n" + "\n".join(f"• {dep}" for dep in missing_deps)
desc_text += "\n\nBu gereksinimler olmadan uygulama tam çalışmayabilir."
desc_label = ctk.CTkLabel(dialog, text=desc_text, wraplength=450)
desc_label.pack(pady=(0, 20))
progress_label, progress_bar, buttons_frame = create_progress_section(dialog)
def download_requirements():
"""Gereksinimleri indir."""
download_btn.configure(state="disabled", text="İndiriliyor...")
skip_btn.configure(state="disabled")
def progress_callback(msg):
progress_label.configure(text=msg)
progress_bar.set(0.5) # Orta değer
def download_worker():
try:
if self.requirements_manager:
results = self.requirements_manager.download_requirements(missing_deps, progress_callback)
# Sonuçları göster
success_count = sum(1 for r in results if r["success"])
total_count = len(results)
if success_count == total_count:
progress_label.configure(text="✅ Tüm gereksinimler başarıyla kuruldu!")
progress_bar.set(1.0)
download_btn.configure(text="✅ Tamamlandı")
else:
failed = [r["name"] for r in results if not r["success"]]
progress_label.configure(text=f"❌ {len(failed)} gereksinim kurulamadı")
progress_bar.set(0.0)
download_btn.configure(text="❌ Hata Oluştu")
# 2 saniye sonra dialog'u kapat
self.after(2000, dialog.destroy)
except Exception as e:
progress_label.configure(text=f"❌ Hata: {str(e)}")
progress_bar.set(0.0)
download_btn.configure(text="❌ Hata")
threading.Thread(target=download_worker, daemon=True).start()
download_btn = ctk.CTkButton(buttons_frame, text="⬇️ Gereksinimleri İndir",
command=download_requirements,
fg_color="#4ecdc4", hover_color="#45b7aa")
download_btn.pack(side="left", padx=(0, 10))
def skip_download():
"""İndirmeyi atla."""
dialog.destroy()
self.message("Gereksinimler indirilmedi. Bazı özellikler çalışmayabilir.", error=True)
skip_btn = ctk.CTkButton(buttons_frame, text="⏭️ Atla",
command=skip_download,
fg_color="#666666")
skip_btn.pack(side="left")
# Dialog'u ortala
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - dialog.winfo_width()) // 2
y = (dialog.winfo_screenheight() - dialog.winfo_height()) // 2
dialog.geometry(f"+{x}+{y}")
def open_anime_search_dialog(self):
"""Anime arama diyalog penceresi aç."""
# Dialog penceresi oluştur
dialog = ctk.CTkToplevel(self)
dialog.title("Anime Ara")
dialog.geometry("600x500")
dialog.transient(self)
dialog.grab_set()
# Başlık
title_label = ctk.CTkLabel(dialog, text="🔍 Anime Ara",
font=ctk.CTkFont(size=20, weight="bold"),
text_color="#ffffff")
title_label.pack(pady=(20, 10))
# Arama giriş alanı
search_frame = ctk.CTkFrame(dialog, fg_color="transparent")
search_frame.pack(fill="x", padx=20, pady=(0, 10))
search_label = ctk.CTkLabel(search_frame, text="Anime adı:",
font=ctk.CTkFont(size=14))
search_label.pack(anchor="w", pady=(0, 5))
self.search_entry = ctk.CTkEntry(search_frame, placeholder_text="Anime adını yazın...",
font=ctk.CTkFont(size=14), height=40)
self.search_entry.pack(fill="x", pady=(0, 10))
self.search_entry.focus()
# Adaptör seçimi
adapter_frame = ctk.CTkFrame(dialog, fg_color="transparent")
adapter_frame.pack(fill="x", padx=20, pady=(0, 10))
adapter_label = ctk.CTkLabel(adapter_frame, text="Arama Kaynağı:",
font=ctk.CTkFont(size=14))
adapter_label.pack(anchor="w", pady=(0, 5))
# Mevcut kaynak seçimini kullan (TürkAnime/AnimeciX)
current_source = getattr(self, 'selected_source', 'AnimeciX')
adapter_options = ["AnimeciX", "TürkAnime", "AniList"]
self.adapter_combo = ctk.CTkComboBox(adapter_frame, values=adapter_options,
width=200, height=35,
command=self._on_adapter_change)
self.adapter_combo.pack(side="left", padx=(0, 10))
# Varsayılan seçimi ayarla
if current_source in adapter_options:
self.adapter_combo.set(current_source)
else:
self.adapter_combo.set("AnimeciX")
# Kaynak değiştir butonu
switch_btn = ctk.CTkButton(adapter_frame, text="🔄 Kaynak Değiştir",
command=self._switch_search_source,
fg_color="#4ecdc4", hover_color="#45b7aa",
width=120, height=35)
switch_btn.pack(side="left")
# Sonuç listesi
results_frame = ctk.CTkFrame(dialog, fg_color="#1a1a1a")
results_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20))
results_label = ctk.CTkLabel(results_frame, text="Arama Sonuçları:",
font=ctk.CTkFont(size=14, weight="bold"))
results_label.pack(anchor="w", padx=15, pady=(15, 10))
# Scrollable frame for results
self.results_scrollable = ctk.CTkScrollableFrame(results_frame, fg_color="transparent")
self.results_scrollable.pack(fill="both", expand=True, padx=15, pady=(0, 15))
# Loading label
self.loading_label = ctk.CTkLabel(self.results_scrollable, text="Arama yapmak için yazmaya başlayın...",
font=ctk.CTkFont(size=12),
text_color="#888888")
self.loading_label.pack(pady=20)
# Butonlar
buttons_frame = ctk.CTkFrame(dialog, fg_color="transparent")
buttons_frame.pack(fill="x", padx=20, pady=(0, 20))
cancel_btn = ctk.CTkButton(buttons_frame, text="❌ İptal",
command=dialog.destroy,
fg_color="#666666", width=100)
cancel_btn.pack(side="right")
# Arama değişkenlerini sakla
self.search_timer = None
self.current_results = []
# Event binding - arama girişi değiştiğinde
self.search_entry.bind("<KeyRelease>", lambda e: self._on_search_input_change(dialog))
# Dialog'u ortala
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - dialog.winfo_width()) // 2
y = (dialog.winfo_screenheight() - dialog.winfo_height()) // 2
dialog.geometry(f"+{x}+{y}")
# Dialog kapatıldığında timer'ı iptal et
def on_dialog_close():
if hasattr(self, 'search_timer') and self.search_timer:
self.after_cancel(self.search_timer)
self.search_timer = None
dialog.destroy()
dialog.protocol("WM_DELETE_WINDOW", on_dialog_close)
def _on_adapter_change(self, selected_adapter):
"""Adaptör seçildiğinde çağrılır."""
self.selected_search_adapter = selected_adapter
print(f"Seçilen arama adaptörü: {selected_adapter}")
def _switch_search_source(self):
"""Arama kaynağını değiştir."""
if hasattr(self, 'selected_search_adapter'):
# Ana uygulama kaynağını güncelle
if self.selected_search_adapter in ["TürkAnime", "AnimeciX"]:
self.selected_source = self.selected_search_adapter
# Ana kaynak selector'ünü güncelle
if hasattr(self, 'cmbSource'):
self.cmbSource.set(self.selected_search_adapter)
self.message(f"Arama kaynağı {self.selected_search_adapter} olarak değiştirildi!")
else:
self.message("Bu kaynak sadece arama için kullanılabilir", error=True)
def _on_search_input_change(self, dialog):
"""Arama girişi değiştiğinde çağrılır."""
# Önceki timer'ı iptal et
if hasattr(self, 'search_timer') and self.search_timer:
self.after_cancel(self.search_timer)
# Yeni timer başlat (300ms gecikme)
self.search_timer = self.after(300, lambda: self._perform_search(dialog))
def _perform_search(self, dialog):
"""Anime arama işlemini gerçekleştir - tüm kaynaklarda paralel arama."""
query = self.search_entry.get().strip()
if not query or len(query) < 2:
self._clear_results()
self.loading_label.configure(text="En az 2 karakter yazın...")
self.loading_label.pack(pady=20)
return
# Arama iptal mekanizması: mevcut aramanın sürümünü takip et
if not hasattr(self, '_search_version'):
self._search_version = 0
self._search_version += 1
current_version = self._search_version
# Loading göster
self._clear_results()
self.loading_label.configure(text="🔍 Tüm kaynaklarda aranıyor...")
self.loading_label.pack(pady=20)
# Arama işlemini thread'de çalıştır
def search_worker():
try:
# Tüm kaynaklarda paralel arama yap
all_results = search_engine.search_all_sources(query, limit_per_source=10)
# İptal edildi mi kontrol et
if getattr(self, '_search_version', 0) != current_version:
return
# Sonuçları birleştir ve kaynak bilgisi ekle
combined_results = []
for source_name, results in all_results.items():
for anime_id, anime_name in results:
combined_results.append({
'id': anime_id,
'name': anime_name,
'source': source_name
})
# Ana thread'de sonuçları göster
self.after(0, lambda: self._display_combined_search_results(combined_results, dialog))
except Exception as e:
self.after(0, lambda err=e: self._show_search_error(str(err), dialog))
threading.Thread(target=search_worker, daemon=True).start()
def _clear_results(self):
"""Sonuçları temizle."""
# Mevcut sonuç widget'larını temizle
for widget in self.results_scrollable.winfo_children():
if widget != self.loading_label:
widget.destroy()
def _display_search_results(self, results, dialog):
"""Arama sonuçlarını göster."""
self._clear_results()
self.current_results = results
if not results:
no_results = ctk.CTkLabel(self.results_scrollable, text="Sonuç bulunamadı",
font=ctk.CTkFont(size=14),
text_color="#888888")
no_results.pack(pady=20)
return
# Sonuçları göster
for i, (anime_id, anime_name) in enumerate(results[:20]): # Max 20 sonuç
result_frame = ctk.CTkFrame(self.results_scrollable, fg_color="#2a2a2a",
border_width=1, border_color="#444444")
result_frame.pack(fill="x", pady=2, padx=5)
# Anime adı
name_label = ctk.CTkLabel(result_frame, text=anime_name,
font=ctk.CTkFont(size=13),
text_color="#ffffff",
anchor="w")
name_label.pack(side="left", fill="x", expand=True, padx=10, pady=8)
# ID göster
id_label = ctk.CTkLabel(result_frame, text=f"#{anime_id}",
font=ctk.CTkFont(size=11),
text_color="#cccccc")
id_label.pack(side="right", padx=10, pady=8)
# Tıklama eventi
def make_click_handler(aid=anime_id, aname=anime_name, dlg=dialog):
return lambda e: self._on_anime_selected(aid, aname, dlg)
result_frame.bind("<Button-1>", make_click_handler(aid=anime_id, aname=anime_name, dlg=dialog))
name_label.bind("<Button-1>", make_click_handler(aid=anime_id, aname=anime_name, dlg=dialog))
id_label.bind("<Button-1>", make_click_handler(aid=anime_id, aname=anime_name, dlg=dialog))
# Hover efekti
def on_enter(e, frame=result_frame):
frame.configure(fg_color="#3a3a3a")
def on_leave(e, frame=result_frame):
frame.configure(fg_color="#2a2a2a")
result_frame.bind("<Enter>", on_enter)
result_frame.bind("<Leave>", on_leave)
def _display_combined_search_results(self, results, dialog):
"""Birleştirilmiş arama sonuçlarını göster - kaynak bilgisi ile."""
self._clear_results()
self.current_results = results
if not results:
no_results = ctk.CTkLabel(self.results_scrollable, text="Sonuç bulunamadı",
font=ctk.CTkFont(size=14),
text_color="#888888")
no_results.pack(pady=20)
return
# Sonuçları kaynaklara göre grupla
source_groups = {}
for result in results:
source = result['source']
if source not in source_groups:
source_groups[source] = []
source_groups[source].append(result)
# Her kaynak için sonuçları göster
for source_name, source_results in source_groups.items():
# Kaynak başlığı
source_label = ctk.CTkLabel(self.results_scrollable,
text=f"📺 {source_name} ({len(source_results)} sonuç)",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#4ecdc4")
source_label.pack(anchor="w", padx=5, pady=(10, 5))
# Kaynak sonuçları
for result in source_results[:10]: # Kaynak başına max 10 sonuç
result_frame = ctk.CTkFrame(self.results_scrollable, fg_color="#2a2a2a",
border_width=1, border_color="#444444")
result_frame.pack(fill="x", pady=2, padx=10)
# Anime adı
name_label = ctk.CTkLabel(result_frame, text=result['name'],
font=ctk.CTkFont(size=13),
text_color="#ffffff",
anchor="w")
name_label.pack(side="left", fill="x", expand=True, padx=10, pady=8)
# Kaynak ve ID göster
info_label = ctk.CTkLabel(result_frame,
text=f"{result['source']} #{result['id']}",
font=ctk.CTkFont(size=11),
text_color="#cccccc")
info_label.pack(side="right", padx=10, pady=8)
# Tıklama eventi
def make_click_handler(aid=result['id'], aname=result['name'],
source=result['source'], dlg=dialog):
return lambda e: self._on_anime_selected_with_source(aid, aname, source, dlg)
result_frame.bind("<Button-1>", make_click_handler(aid=result['id'],
aname=result['name'],
source=result['source'], dlg=dialog))
name_label.bind("<Button-1>", make_click_handler(aid=result['id'],
aname=result['name'],
source=result['source'], dlg=dialog))
info_label.bind("<Button-1>", make_click_handler(aid=result['id'],
aname=result['name'],
source=result['source'], dlg=dialog))
# Hover efekti
def on_enter(e, frame=result_frame):
frame.configure(fg_color="#3a3a3a")
def on_leave(e, frame=result_frame):
frame.configure(fg_color="#2a2a2a")
result_frame.bind("<Enter>", on_enter)
result_frame.bind("<Leave>", on_leave)
def _show_search_error(self, error_msg, dialog):
"""Arama hatası göster."""
self._clear_results()
error_label = ctk.CTkLabel(self.results_scrollable,
text=f"Arama hatası: {error_msg}",
font=ctk.CTkFont(size=12),
text_color="#ff6b6b")
error_label.pack(pady=20)
def _on_anime_selected(self, anime_id, anime_name, dialog):
"""Anime seçildiğinde çağrılır."""
try:
# JSON dosyasına kaydet
self._save_anime_to_json(anime_id, anime_name)
# Ana pencereyle eşleştir
self._match_anime_with_main(anime_id, anime_name)
# Dialog'u kapat
dialog.destroy()
# Başarı mesajı
self.message(f"'{anime_name}' seçildi ve kaydedildi!")
except Exception as e:
self.message(f"Anime seçme hatası: {e}", error=True)
def _on_anime_selected_with_source(self, anime_id, anime_name, source, dialog):
"""Kaynak bilgisi ile anime seçildiğinde çağrılır."""
try:
# JSON dosyasına kaydet
self._save_anime_to_json(anime_id, anime_name)
# Veritabanına kaydet (eğer API mevcutsa)
try:
db = APIManager()
db.save_anime_match(source, anime_id, anime_name)
except Exception as db_e:
print(f"API kaydetme hatası (normal): {db_e}")
# Ana pencereyle eşleştir - kaynak bilgisi ile
self._match_anime_with_main_and_source(anime_id, anime_name, source)
# Dialog'u kapat
dialog.destroy()
# Başarı mesajı
self.message(f"'{anime_name}' ({source}) seçildi ve kaydedildi!")
except Exception as e:
self.message(f"Anime seçme hatası: {e}", error=True)
def _save_anime_to_json(self, anime_id, anime_name):
"""Anime'yi anime_names.json dosyasına kaydet."""
try:
json_path = os.path.join(os.path.dirname(__file__), "anime_names.json")
# Mevcut veriyi oku
existing_data = {}
if os.path.exists(json_path):
try:
with open(json_path, 'r', encoding='utf-8') as f:
existing_data = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
existing_data = {}
# Tekrar eden kayıt kontrolü
if str(anime_id) in existing_data:
# Zaten varsa güncelleme yap
existing_data[str(anime_id)]['name'] = anime_name
existing_data[str(anime_id)]['last_updated'] = time.time()
else:
# Yeni kayıt ekle
existing_data[str(anime_id)] = {
'name': anime_name,
'added_date': time.time(),
'last_updated': time.time()
}
# JSON dosyasına yaz
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(existing_data, f, ensure_ascii=False, indent=2)
except Exception as e:
raise Exception(f"JSON kaydetme hatası: {e}")
def _match_anime_with_main(self, anime_id, anime_name):
"""Seçilen anime'yi ana pencereyle eşleştir."""
try:
# Animecix'ten detayları al
from turkanime_api.sources.animecix import CixAnime
# anime_id'yi güvenli şekilde int'e çevir
safe_anime_id = self._safe_int_conversion(anime_id)
# Yeni anime nesnesi oluştur
anime_obj = CixAnime(str(safe_anime_id), anime_name)
# AniList eşleştirmesi için basit veri oluştur
anilist_data = {
'id': safe_anime_id,
'title': {
'romaji': anime_name,
'english': anime_name
},
'episodes': None, # Bilinmiyor
'coverImage': {'large': None},
'averageScore': None,
'popularity': None,
'description': f"{anime_name} - Animecix'ten eklendi"
}
# Ana pencerede göster
self.show_anime_details(anilist_data)
except Exception as e:
raise Exception(f"Ana pencere eşleştirme hatası: {e}")
def _safe_int_conversion(self, value, default=0):
"""Güvenli int dönüşümü - string ID'ler için."""
try:
return int(value)
except (ValueError, TypeError):
# String ID ise hash değeri al veya default kullan
if isinstance(value, str) and value:
# String ID için basit bir hash oluştur
return hash(value) % 1000000 # 6 haneli sayı
return default
def _match_anime_with_main_and_source(self, anime_id, anime_name, source):
"""Kaynak bilgisi ile seçilen anime'yi ana pencereyle eşleştir."""
try:
# anime_id'yi güvenli şekilde int'e çevir
safe_anime_id = self._safe_int_conversion(anime_id)
# Kaynağa göre uygun anime nesnesi oluştur
if source == "AnimeciX":
from turkanime_api.sources.animecix import CixAnime
anime_obj = CixAnime(str(safe_anime_id), anime_name)
anilist_data = {
'id': safe_anime_id,
'title': {
'romaji': anime_name,
'english': anime_name
},
'episodes': len(anime_obj.episodes) if hasattr(anime_obj, 'episodes') else None,
'coverImage': {'large': None},
'averageScore': None,
'popularity': None,
'description': f"{anime_name} - {source}'ten eklendi"
}
elif source == "TürkAnime":
from turkanime_api.objects import Anime
anime_obj = Anime(anime_id) # TürkAnime string ID kabul edebilir
anilist_data = {
'id': safe_anime_id,
'title': {
'romaji': anime_obj.title if hasattr(anime_obj, 'title') else anime_name,
'english': anime_name
},
'episodes': len(anime_obj.bolumler) if hasattr(anime_obj, 'bolumler') else None,
'coverImage': {'large': None},
'averageScore': None,
'popularity': None,
'description': f"{anime_name} - {source}'ten eklendi"
}
elif source == "AniList":
# AniList için adaptör kullanarak detayları al
adapter = search_engine.get_adapter("AniList")
anime_details = None
if adapter:
anime_details = adapter.get_anime_details(str(anime_id))
if anime_details:
anilist_data = {
'id': safe_anime_id,
'title': {
'romaji': anime_details.get('title', anime_name),
'english': anime_name
},
'episodes': anime_details.get('episodes'),
'coverImage': {'large': None},
'averageScore': None,
'popularity': None,
'description': f"{anime_name} - {source}'ten eklendi"
}
else:
# Fallback
anilist_data = {
'id': safe_anime_id,
'title': {
'romaji': anime_name,
'english': anime_name
},
'episodes': None,
'coverImage': {'large': None},
'averageScore': None,
'popularity': None,
'description': f"{anime_name} - {source}'ten eklendi"
}
else:
# Bilinmeyen kaynak için varsayılan
anilist_data = {
'id': safe_anime_id,
'title': {
'romaji': anime_name,
'english': anime_name
},
'episodes': None,
'coverImage': {'large': None},
'averageScore': None,
'popularity': None,
'description': f"{anime_name} - {source}'ten eklendi"
}
# Ana pencerede göster
self.show_anime_details(anilist_data)
except Exception as e:
raise Exception(f"Ana pencere eşleştirme hatası ({source}): {e}")
def get_anime_titles(self):
"""Mevcut anime'den başlıkları al (Romanji, İngilizce, vb.)"""
titles = []
if hasattr(self, 'selected_anime') and self.selected_anime:
anime_data = self.selected_anime
title_data = anime_data.get('title', {})
# Romanji başlığı
romaji = title_data.get('romaji')
if romaji:
titles.append(f"🇯🇵 Romanji: {romaji}")
# İngilizce başlığı
english = title_data.get('english')
if english:
titles.append(f"🇺🇸 İngilizce: {english}")
# Diğer diller
native = title_data.get('native')
if native:
titles.append(f"🇯🇵 Orijinal: {native}")
# Eğer hiç başlık yoksa varsayılan ekle
if not titles:
titles = ["🇺🇸 İngilizce: Bilinmiyor", "🇯🇵 Romanji: Bilinmiyor"]
return titles
def update_title_selector(self):
"""Başlık selector'ünü güncelle."""
if hasattr(self, 'cmbTitle'):
titles = self.get_anime_titles()
self.cmbTitle.configure(values=titles)
# Default olarak İngilizce'yi seç
english_title = None
for title in titles:
if "İngilizce:" in title:
english_title = title
break
if english_title:
self.cmbTitle.set(english_title)
elif titles:
self.cmbTitle.set(titles[0])
def on_title_change(self, selected_title):
"""Başlık seçildiğinde çağrılır."""
if selected_title:
# Seçilen başlığı sakla
self.selected_title = selected_title
print(f"Seçilen başlık: {selected_title}")
def on_open_settings(self):
"""Ayarlar panelini içeride aç."""
# Mevcut içeriği temizle
self.clear_content_area()
# Başlık frame
title_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
title_frame.pack(fill="x", pady=(20, 20))
back_btn = ctk.CTkButton(title_frame, text="← Ana Sayfa",
command=self.show_home,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"))
back_btn.pack(side="left")
title_label = ctk.CTkLabel(title_frame, text="⚙️ Ayarlar",
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
title_label.pack(side="left", padx=30)
# İki sütunlu düzen
columns_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
columns_frame.pack(fill="both", expand=True, padx=20)
# Sol sütun
left_column = ctk.CTkFrame(columns_frame, fg_color="transparent")
left_column.pack(side="left", fill="both", expand=True, padx=(0, 10))
# Sağ sütun
right_column = ctk.CTkFrame(columns_frame, fg_color="transparent")
right_column.pack(side="left", fill="both", expand=True, padx=(10, 0))
# Dosya ayarları
self.dosya = Dosyalar()
a = self.dosya.ayarlar
# === SOL SÜTUN ===
# İndirme ayarları
download_frame = ctk.CTkFrame(left_column, fg_color="#1a1a1a", corner_radius=12)
download_frame.pack(fill="x", pady=(0, 15))
download_header = ctk.CTkFrame(download_frame, fg_color="#ff6b6b", corner_radius=8, height=40)
download_header.pack(fill="x", padx=3, pady=3)
download_header.pack_propagate(False)
ctk.CTkLabel(download_header, text="⬇️ İndirme Ayarları",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff").pack(side="left", padx=15, pady=8)
# Paralel indirme sayısı
parallel_frame = ctk.CTkFrame(download_frame, fg_color="transparent")
parallel_frame.pack(fill="x", padx=15, pady=8)
ctk.CTkLabel(parallel_frame, text="Paralel indirme sayısı:",
font=ctk.CTkFont(size=12)).pack(side="left")
self.spinParallel = ctk.CTkEntry(parallel_frame, width=80, height=30)
self.spinParallel.pack(side="right")
self.spinParallel.insert(0, str(a.get("paralel indirme sayisi", 3)))
# 1080p aday sayısı
early_frame = ctk.CTkFrame(download_frame, fg_color="transparent")
early_frame.pack(fill="x", padx=15, pady=8)
ctk.CTkLabel(early_frame, text="1080p aday sayısı:",
font=ctk.CTkFont(size=12)).pack(side="left")
self.spinEarlySubset = ctk.CTkEntry(early_frame, width=80, height=30)
self.spinEarlySubset.pack(side="right")
self.spinEarlySubset.insert(0, str(a.get("1080p aday sayısı", 8)))
# Checkboxlar
checkbox_frame = ctk.CTkFrame(download_frame, fg_color="transparent")
checkbox_frame.pack(fill="x", padx=15, pady=(8, 15))
self.chkMaxRes = ctk.CTkCheckBox(checkbox_frame, text="Maksimum çözünürlük",
font=ctk.CTkFont(size=11))
self.chkMaxRes.pack(anchor="w", pady=3)
if a.get("max resolution", True):
self.chkMaxRes.select()
self.chkAria2 = ctk.CTkCheckBox(checkbox_frame, text="Aria2c kullan (hızlı indirme)",
font=ctk.CTkFont(size=11))
self.chkAria2.pack(anchor="w", pady=3)
if a.get("aria2c kullan", False):
self.chkAria2.select()
# Oynatma ayarları
play_frame = ctk.CTkFrame(left_column, fg_color="#1a1a1a", corner_radius=12)
play_frame.pack(fill="x", pady=(0, 15))
play_header = ctk.CTkFrame(play_frame, fg_color="#4ecdc4", corner_radius=8, height=40)
play_header.pack(fill="x", padx=3, pady=3)
play_header.pack_propagate(False)
ctk.CTkLabel(play_header, text="▶️ Oynatma Ayarları",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff").pack(side="left", padx=15, pady=8)
play_checkbox_frame = ctk.CTkFrame(play_frame, fg_color="transparent")
play_checkbox_frame.pack(fill="x", padx=15, pady=15)
self.chkManuel = ctk.CTkCheckBox(play_checkbox_frame, text="Manuel fansub seçimi",
font=ctk.CTkFont(size=11))
self.chkManuel.pack(anchor="w", pady=3)
if a.get("manuel fansub", False):
self.chkManuel.select()
self.chkSaveWhileWatch = ctk.CTkCheckBox(play_checkbox_frame, text="İzlerken kaydet",
font=ctk.CTkFont(size=11))
self.chkSaveWhileWatch.pack(anchor="w", pady=3)
if a.get("izlerken kaydet", False):
self.chkSaveWhileWatch.select()
self.chkRememberMin = ctk.CTkCheckBox(play_checkbox_frame, text="Kaldığın dakikayı hatırla",
font=ctk.CTkFont(size=11))
self.chkRememberMin.pack(anchor="w", pady=3)
if a.get("dakika hatirla", True):
self.chkRememberMin.select()
self.chkWatchedIcon = ctk.CTkCheckBox(play_checkbox_frame, text="İzlendi ikonu göster",
font=ctk.CTkFont(size=11))
self.chkWatchedIcon.pack(anchor="w", pady=3)
if a.get("izlendi ikonu", True):
self.chkWatchedIcon.select()
self.chkDiscordRPC = ctk.CTkCheckBox(play_checkbox_frame, text="Discord Rich Presence",
font=ctk.CTkFont(size=11))
self.chkDiscordRPC.pack(anchor="w", pady=3)
if a.get("discord_rich_presence", False):
self.chkDiscordRPC.select()
# === SAĞ SÜTUN ===
# AniList OAuth ayarları
anilist_frame = ctk.CTkFrame(right_column, fg_color="#1a1a1a", corner_radius=12)
anilist_frame.pack(fill="x", pady=(0, 15))
anilist_header = ctk.CTkFrame(anilist_frame, fg_color="#02a9ff", corner_radius=8, height=40)
anilist_header.pack(fill="x", padx=3, pady=3)
anilist_header.pack_propagate(False)
ctk.CTkLabel(anilist_header, text="🔗 AniList Entegrasyonu",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff").pack(side="left", padx=15, pady=8)
anilist_content = ctk.CTkFrame(anilist_frame, fg_color="transparent")
anilist_content.pack(fill="x", padx=15, pady=15)
# Client ID
ctk.CTkLabel(anilist_content, text="Client ID:",
font=ctk.CTkFont(size=11)).pack(anchor="w")
self.txtAniClientId = ctk.CTkEntry(anilist_content, width=250, height=30)
self.txtAniClientId.pack(anchor="w", pady=(3, 10))
try:
from turkanime_api.anilist_client import anilist_client as _ac
self.txtAniClientId.insert(0, str(getattr(_ac, 'client_id', '')))
except:
pass
# Client Secret
ctk.CTkLabel(anilist_content, text="Client Secret:",
font=ctk.CTkFont(size=11)).pack(anchor="w")
self.txtAniClientSecret = ctk.CTkEntry(anilist_content, width=250, height=30, show="•")
self.txtAniClientSecret.pack(anchor="w", pady=(3, 10))
try:
self.txtAniClientSecret.insert(0, str(getattr(_ac, 'client_secret', '')))
except:
pass
# Redirect URI
ctk.CTkLabel(anilist_content, text="Redirect URI:",
font=ctk.CTkFont(size=11)).pack(anchor="w")
self.txtAniRedirectUri = ctk.CTkEntry(anilist_content, width=250, height=30)
self.txtAniRedirectUri.pack(anchor="w", pady=(3, 5))
try:
self.txtAniRedirectUri.insert(0, str(getattr(_ac, 'redirect_uri', 'http://localhost:9921/anilist-login')))
except:
pass
# User Tracking ayarları
tracking_frame = ctk.CTkFrame(right_column, fg_color="#1a1a1a", corner_radius=12)
tracking_frame.pack(fill="x", pady=(0, 15))
tracking_header = ctk.CTkFrame(tracking_frame, fg_color="#e17055", corner_radius=8, height=40)
tracking_header.pack(fill="x", padx=3, pady=3)
tracking_header.pack_propagate(False)
ctk.CTkLabel(tracking_header, text="📊 Kullanıcı İzleme",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff").pack(side="left", padx=15, pady=8)
tracking_content = ctk.CTkFrame(tracking_frame, fg_color="transparent")
tracking_content.pack(fill="x", padx=15, pady=15)
ctk.CTkLabel(tracking_content, text="User ID (izleme senkronizasyonu):",
font=ctk.CTkFont(size=11)).pack(anchor="w")
user_id_frame = ctk.CTkFrame(tracking_content, fg_color="transparent")
user_id_frame.pack(fill="x", pady=(3, 10))
self.txtUserId = ctk.CTkEntry(user_id_frame, width=200, height=30,
placeholder_text="Otomatik oluşturulur")
self.txtUserId.pack(side="left")
current_user_id = a.get("user_id", "")
if current_user_id:
self.txtUserId.insert(0, current_user_id)
ctk.CTkButton(user_id_frame, text="🔄", width=35, height=30,
command=self.generate_new_user_id,
fg_color="#e17055", hover_color="#d63031").pack(side="left", padx=5)
ctk.CTkLabel(tracking_content, text="ℹ️ User ID ile izlediğin bölümler\nfarklı cihazlarda senkronize edilir.",
font=ctk.CTkFont(size=10), text_color="#888888").pack(anchor="w")
# TRAnimeİzle Cookie ayarları
tranime_frame = ctk.CTkFrame(right_column, fg_color="#1a1a1a", corner_radius=12)
tranime_frame.pack(fill="x", pady=(0, 15))
tranime_header = ctk.CTkFrame(tranime_frame, fg_color="#e84393", corner_radius=8, height=40)
tranime_header.pack(fill="x", padx=3, pady=3)
tranime_header.pack_propagate(False)
ctk.CTkLabel(tranime_header, text="🌐 TRAnimeİzle Cookie",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff").pack(side="left", padx=15, pady=8)
tranime_content = ctk.CTkFrame(tranime_frame, fg_color="transparent")
tranime_content.pack(fill="x", padx=15, pady=15)
ctk.CTkLabel(tranime_content, text="TRAnimeİzle Cookie (Netscape veya .AitrWeb.Session):",
font=ctk.CTkFont(size=11)).pack(anchor="w")
self.txtTranimeCookie = ctk.CTkTextbox(tranime_content, width=250, height=80,
font=ctk.CTkFont(size=10))
self.txtTranimeCookie.pack(anchor="w", pady=(3, 10), fill="x")
tranime_cookie_val = a.get("tranime_cookie", "")
if tranime_cookie_val:
self.txtTranimeCookie.insert("0.0", tranime_cookie_val)
ctk.CTkLabel(tranime_content,
text="ℹ️ Netscape cookie dosyasının içeriğini veya\nsadece .AitrWeb.Session değerini yapıştırın.\nNetscape formatı tüm cookie'leri otomatik parse eder.",
font=ctk.CTkFont(size=10), text_color="#888888",
justify="left").pack(anchor="w")
cookie_buttons_frame = ctk.CTkFrame(tranime_content, fg_color="transparent")
cookie_buttons_frame.pack(anchor="w", pady=(8, 0))
if cookie_browser_available():
ctk.CTkButton(cookie_buttons_frame, text="🌐 Otomatik Cookie Al",
width=160, height=28, font=ctk.CTkFont(size=11),
fg_color="#00b894", hover_color="#00a381",
command=self.start_cookie_browser).pack(side="left", padx=(0, 8))
ctk.CTkButton(cookie_buttons_frame, text="❓ Nasıl Alınır?",
width=130, height=28, font=ctk.CTkFont(size=11),
fg_color="#6c5ce7", hover_color="#5a4bd1",
command=self.show_cookie_tutorial).pack(side="left")
# FlareSolverr
flare_frame = ctk.CTkFrame(right_column, fg_color="#1a1a1a", corner_radius=12)
flare_frame.pack(fill="x", pady=(0, 15))
flare_header = ctk.CTkFrame(flare_frame, fg_color="#fd79a8", corner_radius=8, height=40)
flare_header.pack(fill="x", padx=3, pady=3)
flare_header.pack_propagate(False)
ctk.CTkLabel(flare_header, text="🛡️ FlareSolverr (CF Bypass)",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff").pack(side="left", padx=15, pady=8)
flare_content = ctk.CTkFrame(flare_frame, fg_color="transparent")
flare_content.pack(fill="x", padx=15, pady=15)
ctk.CTkLabel(flare_content, text="FlareSolverr URL:",
font=ctk.CTkFont(size=11)).pack(anchor="w")
self.txtFlaresolverrUrl = ctk.CTkEntry(flare_content, width=250, height=30,
placeholder_text="http://localhost:8191")
self.txtFlaresolverrUrl.pack(anchor="w", pady=(3, 5))
flare_url_val = a.get("flaresolverr_url", "")
if flare_url_val:
self.txtFlaresolverrUrl.insert(0, flare_url_val)
ctk.CTkLabel(flare_content,
text="ℹ️ Cloudflare korumasını aşmak için FlareSolverr\nkullanılır. Boş bırakırsanız varsayılan\nsunucu kullanılır.",
font=ctk.CTkFont(size=10), text_color="#888888",
justify="left").pack(anchor="w")
# İndirme klasörü
folder_frame = ctk.CTkFrame(right_column, fg_color="#1a1a1a", corner_radius=12)
folder_frame.pack(fill="x", pady=(0, 15))
folder_header = ctk.CTkFrame(folder_frame, fg_color="#a29bfe", corner_radius=8, height=40)
folder_header.pack(fill="x", padx=3, pady=3)
folder_header.pack_propagate(False)
ctk.CTkLabel(folder_header, text="📁 Klasör Ayarları",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff").pack(side="left", padx=15, pady=8)
folder_content = ctk.CTkFrame(folder_frame, fg_color="transparent")
folder_content.pack(fill="x", padx=15, pady=15)
ctk.CTkLabel(folder_content, text="İndirme klasörü:",
font=ctk.CTkFont(size=11)).pack(anchor="w")
folder_entry_frame = ctk.CTkFrame(folder_content, fg_color="transparent")
folder_entry_frame.pack(fill="x", pady=(3, 0))
self.txtDownloadFolder = ctk.CTkEntry(folder_entry_frame, width=200, height=30)
self.txtDownloadFolder.pack(side="left")
self.txtDownloadFolder.insert(0, a.get("indirilenler", "."))
ctk.CTkButton(folder_entry_frame, text="📂", width=35, height=30,
command=self.browse_download_folder).pack(side="left", padx=5)
# Kaydet butonu
save_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
save_frame.pack(fill="x", pady=20, padx=20)
ctk.CTkButton(save_frame, text="💾 Ayarları Kaydet",
command=self.save_settings,
fg_color="#ff6b6b", hover_color="#ff5252",
width=200, height=40, corner_radius=20,
font=ctk.CTkFont(size=14, weight="bold")).pack(side="right")
def browse_download_folder(self):
"""İndirme klasörü seç."""
from tkinter import filedialog
folder = filedialog.askdirectory()
if folder:
self.txtDownloadFolder.delete(0, "end")
self.txtDownloadFolder.insert(0, folder)
def _prompt_cookie_browser(self):
"""İlk açılışta TRAnimeİzle cookie'si yoksa kullanıcıya sor."""
# Bir daha sorma
self.dosya.set_ayar("cookie_browser_prompted", True)
if not cookie_browser_available():
return
dialog = ctk.CTkToplevel(self)
dialog.title("🍪 TRAnimeİzle Cookie")
dialog.geometry("440x260")
dialog.resizable(False, False)
dialog.configure(fg_color="#0f0f0f")
dialog.attributes("-topmost", True)
dialog.grab_set()
ctk.CTkLabel(dialog, text="🍪 TRAnimeİzle Cookie Gerekli",
font=ctk.CTkFont(size=18, weight="bold"),
text_color="#e84393").pack(pady=(25, 8))
ctk.CTkLabel(dialog,
text="TRAnimeİzle kaynağını kullanabilmek için\n"
"oturum cookie'si gereklidir.\n\n"
"Tarayıcı açılarak cookie'ler otomatik alınabilir.\n"
"(Captcha çıkarsa çözmeniz yeterli)",
font=ctk.CTkFont(size=12), text_color="#aaaaaa",
justify="center").pack(pady=(0, 20))
btn_frame = ctk.CTkFrame(dialog, fg_color="transparent")
btn_frame.pack(pady=(0, 25))
def accept():
dialog.destroy()
self.start_cookie_browser()
def decline():
dialog.destroy()
ctk.CTkButton(btn_frame, text="✅ Evet, Otomatik Al",
width=160, height=38,
font=ctk.CTkFont(size=13, weight="bold"),
fg_color="#00b894", hover_color="#00a381",
command=accept).pack(side="left", padx=(0, 12))
ctk.CTkButton(btn_frame, text="Hayır, Sonra",
width=120, height=38,
font=ctk.CTkFont(size=13),
fg_color="#636e72", hover_color="#535c60",
command=decline).pack(side="left")
dialog.protocol("WM_DELETE_WINDOW", decline)
def start_cookie_browser(self):
"""🌐 Selenium tarayıcı ile otomatik cookie toplama."""
# Zaten çalışıyorsa engelle
if hasattr(self, '_cookie_worker') and self._cookie_worker and self._cookie_worker.is_running:
messagebox.showinfo("Bilgi", "Cookie tarayıcısı zaten çalışıyor.")
return
# Dialog penceresi
self._cookie_dialog = ctk.CTkToplevel(self)
self._cookie_dialog.title("🌐 Otomatik Cookie Toplama")
self._cookie_dialog.geometry("460x320")
self._cookie_dialog.resizable(False, False)
self._cookie_dialog.configure(fg_color="#0f0f0f")
self._cookie_dialog.attributes("-topmost", True)
self._cookie_dialog.grab_set()
# Başlık
ctk.CTkLabel(self._cookie_dialog, text="🌐 TRAnimeİzle Cookie Toplama",
font=ctk.CTkFont(size=18, weight="bold"),
text_color="#00b894").pack(pady=(20, 5))
ctk.CTkLabel(self._cookie_dialog,
text="Tarayıcı açılacak ve cookie'ler otomatik toplanacak.\nCaptcha çıkarsa tarayıcıda çözmeniz yeterli.",
font=ctk.CTkFont(size=12), text_color="#aaaaaa",
justify="center").pack(pady=(0, 15))
# Durum etiketi
status_frame = ctk.CTkFrame(self._cookie_dialog, fg_color="#1a1a1a", corner_radius=12)
status_frame.pack(fill="x", padx=20, pady=(0, 15))
self._cookie_status_label = ctk.CTkLabel(
status_frame,
text="⏳ Hazırlanıyor...",
font=ctk.CTkFont(size=12),
text_color="#ffeaa7",
justify="left",
wraplength=380,
)
self._cookie_status_label.pack(padx=15, pady=15)
# İptal butonu
self._cookie_cancel_btn = ctk.CTkButton(
self._cookie_dialog, text="❌ İptal",
width=120, height=35,
fg_color="#d63031", hover_color="#c0392b",
font=ctk.CTkFont(size=13, weight="bold"),
command=self._cancel_cookie_browser,
)
self._cookie_cancel_btn.pack(pady=(0, 20))
# Eğer gerekli ise otomatik Chromium indirmesi büyük dosya olabilir.
# Kullanıcıya onay sor (ayar kaydetme seçeneğiyle).
allow_download_fn = None
try:
settings = self.dosya.ayarlar
if bool(settings.get("allow_chromium_download", False)):
allow_download_fn = lambda: True
else:
# Modal onay penceresi
choice: Dict[str, Optional[str]] = {"value": None}
dlg = ctk.CTkToplevel(self)
dlg.title("Chromium İndirilsin mi?")
dlg.geometry("520x220")
dlg.resizable(False, False)
# ensure dialog appears on top
try:
dlg.attributes("-topmost", True)
except Exception:
pass
dlg.lift()
dlg.grab_set()
ctk.CTkLabel(dlg, text="🔽 Yerel Chromium gerekli olabilir",
font=ctk.CTkFont(size=15, weight="bold"),
text_color="#ffeaa7").pack(pady=(18, 6))
ctk.CTkLabel(dlg, text="Uygulama yerel Chromium indirmek zorunda kalabilir.\nBu indirme büyük (onlarca MB) olabilir ve uzun sürebilir.\nİzninize ihtiyaç var.",
font=ctk.CTkFont(size=12), text_color="#bbbbbb", justify="center").pack(pady=(0, 12))
btn_frame2 = ctk.CTkFrame(dlg, fg_color="transparent")
btn_frame2.pack(pady=(6, 12))
def _always():
choice["value"] = "always"
try:
self.dosya.set_ayar("allow_chromium_download", True)
except Exception:
pass
dlg.destroy()
def _once():
choice["value"] = "once"
dlg.destroy()
def _never():
choice["value"] = "never"
dlg.destroy()
ctk.CTkButton(btn_frame2, text="✅ Evet — Bir daha sorma",
width=190, height=38, fg_color="#00b894",
command=_always).pack(side="left", padx=(0, 8))
ctk.CTkButton(btn_frame2, text="✅ Evet — Sadece bu sefer",
width=160, height=38, fg_color="#0984e3",
command=_once).pack(side="left", padx=(0, 8))
ctk.CTkButton(btn_frame2, text="❌ Hayır",
width=120, height=38, fg_color="#636e72",
command=_never).pack(side="left")
dlg.protocol("WM_DELETE_WINDOW", _never)
# Wait until dialog closed
self.wait_window(dlg)
if choice["value"] == "always":
allow_download_fn = lambda: True
elif choice["value"] == "once":
allow_download_fn = lambda: True
else:
allow_download_fn = lambda: False
except Exception:
allow_download_fn = None
# Worker başlat
def on_status(msg: str):
try:
self.after(0, lambda: self._cookie_status_label.configure(text=msg))
except Exception:
pass
def on_cookies(netscape_str: str):
def _apply():
try:
# Textbox'a yaz
self.txtTranimeCookie.delete("0.0", "end")
self.txtTranimeCookie.insert("0.0", netscape_str)
# Cookie'yi uygula
set_tranime_cookie(netscape_str)
# Ayarlara kaydet
self.dosya.set_ayar("tranime_cookie", netscape_str)
# Durum güncelle
self._cookie_status_label.configure(
text="✅ Cookie'ler başarıyla kaydedildi!\nArtık bu pencereyi kapatabilirsiniz.",
text_color="#00b894",
)
self._cookie_cancel_btn.configure(text="✔ Kapat", fg_color="#00b894",
hover_color="#00a381")
except Exception as e:
self._cookie_status_label.configure(
text=f"⚠️ Cookie kaydetme hatası: {e}",
text_color="#d63031",
)
self.after(0, _apply)
def on_error(msg: str):
def _show():
try:
self._cookie_status_label.configure(text=f"❌ {msg}", text_color="#d63031")
self._cookie_cancel_btn.configure(text="Kapat", fg_color="#636e72",
hover_color="#535c60")
except Exception:
pass
self.after(0, _show)
# Dispatcher to schedule callbacks on the Tk main thread
def _tk_dispatch(fn, args_tuple, kwargs_dict):
try:
self.after(0, lambda: fn(*args_tuple, **(kwargs_dict or {})))
except Exception:
try:
fn(*args_tuple, **(kwargs_dict or {}))
except Exception:
pass
self._cookie_worker = CookieBrowserWorker(
on_status=on_status,
on_cookies=on_cookies,
on_error=on_error,
dispatch=_tk_dispatch,
allow_download=allow_download_fn,
)
self._cookie_worker.start()
# Dialog kapatılırsa worker'ı da durdur
def on_dialog_close():
self._cancel_cookie_browser()
self._cookie_dialog.protocol("WM_DELETE_WINDOW", on_dialog_close)
def _cancel_cookie_browser(self):
"""Cookie tarayıcı işlemini iptal et."""
if hasattr(self, '_cookie_worker') and self._cookie_worker:
self._cookie_worker.stop()
self._cookie_worker = None
if hasattr(self, '_cookie_dialog') and self._cookie_dialog:
try:
self._cookie_dialog.destroy()
except Exception:
pass
self._cookie_dialog = None
def show_cookie_tutorial(self):
"""🍪 TRAnimeİzle cookie nasıl alınır tutorial dialog'ı."""
tutorial = ctk.CTkToplevel(self)
tutorial.title("🍪 TRAnimeİzle Cookie Rehberi")
tutorial.geometry("520x520")
tutorial.resizable(False, False)
tutorial.configure(fg_color="#0f0f0f")
tutorial.attributes("-topmost", True)
tutorial.grab_set()
# Başlık
ctk.CTkLabel(tutorial, text="🍪 TRAnimeİzle Cookie Nasıl Alınır?",
font=ctk.CTkFont(size=18, weight="bold"),
text_color="#e84393").pack(pady=(20, 5))
ctk.CTkLabel(tutorial, text="TRAnimeİzle kaynağını kullanmak için\noturum cookie'si gereklidir.",
font=ctk.CTkFont(size=12), text_color="#aaaaaa",
justify="center").pack(pady=(0, 15))
# Adımlar
steps_frame = ctk.CTkFrame(tutorial, fg_color="#1a1a1a", corner_radius=12)
steps_frame.pack(fill="x", padx=20, pady=(0, 15))
steps = [
("🌐", "Tarayıcınızda tranimeizle.io adresine gidin"),
("🔑", "Siteye giriş yapın (hesap yoksa kayıt olun)"),
("🛠️", "F12 tuşuna basarak DevTools'u açın"),
("📂", 'Chrome: "Application" sekmesine gidin\nFirefox: "Storage" sekmesine gidin'),
("🍪", 'Sol panelde Cookies → tranimeizle.io seçin'),
("🔍", '.AitrWeb.Session satırını bulun'),
("📋", '"Value" sütunundaki değeri kopyalayın'),
("✅", 'Ayarlar\'daki TRAnimeİzle Cookie alanına yapıştırın'),
]
for i, (icon, text) in enumerate(steps, 1):
step_row = ctk.CTkFrame(steps_frame, fg_color="transparent")
step_row.pack(fill="x", padx=12, pady=(8 if i == 1 else 3, 3 if i < len(steps) else 8))
ctk.CTkLabel(step_row, text=f"{icon} {i}.",
font=ctk.CTkFont(size=12, weight="bold"),
text_color="#e84393", width=40).pack(side="left", anchor="n")
ctk.CTkLabel(step_row, text=text,
font=ctk.CTkFont(size=11), text_color="#dddddd",
justify="left", wraplength=400).pack(side="left", fill="x", padx=(5, 0))
# Not
ctk.CTkLabel(tutorial, text="⚠️ Cookie süresi dolunca yenilemeniz gerekebilir.",
font=ctk.CTkFont(size=10), text_color="#fdcb6e").pack(pady=(0, 10))
# Bir daha gösterme checkbox
dismiss_var = ctk.BooleanVar(value=False)
ctk.CTkCheckBox(tutorial, text="Bir daha gösterme",
variable=dismiss_var, font=ctk.CTkFont(size=11),
text_color="#888888", fg_color="#e84393",
hover_color="#d63031").pack(pady=(0, 10))
def close_tutorial():
if dismiss_var.get():
try:
self.dosya.set_ayar("cookie_tutorial_dismissed", True)
except Exception:
pass
tutorial.destroy()
ctk.CTkButton(tutorial, text="Tamam", width=120, height=35,
fg_color="#e84393", hover_color="#d63031",
font=ctk.CTkFont(size=13, weight="bold"),
command=close_tutorial).pack(pady=(0, 20))
def save_settings(self):
"""Ayarları kaydet."""
try:
self.dosya.set_ayar("manuel fansub", self.chkManuel.get())
self.dosya.set_ayar("izlerken kaydet", self.chkSaveWhileWatch.get())
self.dosya.set_ayar("paralel indirme sayisi", int(self.spinParallel.get()))
self.dosya.set_ayar("max resolution", self.chkMaxRes.get())
self.dosya.set_ayar("1080p aday sayısı", int(self.spinEarlySubset.get()))
self.dosya.set_ayar("dakika hatirla", self.chkRememberMin.get())
self.dosya.set_ayar("aria2c kullan", self.chkAria2.get())
self.dosya.set_ayar("indirilenler", self.txtDownloadFolder.get())
self.dosya.set_ayar("izlendi ikonu", self.chkWatchedIcon.get())
self.dosya.set_ayar("discord_rich_presence", self.chkDiscordRPC.get())
# TRAnimeİzle cookie kaydet ve uygula
tranime_cookie = self.txtTranimeCookie.get("0.0", "end").strip()
self.dosya.set_ayar("tranime_cookie", tranime_cookie)
if tranime_cookie:
try:
set_tranime_cookie(tranime_cookie)
except Exception:
pass
# FlareSolverr URL kaydet
flare_url = self.txtFlaresolverrUrl.get().strip()
self.dosya.set_ayar("flaresolverr_url", flare_url)
# User ID kaydet
user_id = self.txtUserId.get().strip()
if user_id:
self.dosya.set_ayar("user_id", user_id)
# Discord Rich Presence ayarını uygula
if self.chkDiscordRPC.get():
if not self.discord_connected:
self.init_discord_rpc()
if self.discord_connected:
self.message("Discord Rich Presence açıldı", error=False)
else:
if self.discord_connected:
self.disconnect_discord_rpc()
self.message("Discord Rich Presence kapatıldı", error=False)
# AniList ayarlarını kaydet
try:
from turkanime_api.anilist_client import anilist_client as _ac
_ac.client_id = self.txtAniClientId.get()
_ac.client_secret = self.txtAniClientSecret.get()
_ac.redirect_uri = self.txtAniRedirectUri.get()
_ac._save_config()
except:
pass
self.message("✅ Ayarlar kaydedildi!")
except Exception as e:
self.message(f"❌ Ayar kaydetme hatası: {e}", error=True)
def generate_new_user_id(self):
"""Yeni User ID oluştur."""
import uuid
new_id = str(uuid.uuid4())
self.txtUserId.delete(0, "end")
self.txtUserId.insert(0, new_id)
self.message("🔄 Yeni User ID oluşturuldu. Kaydetmeyi unutmayın!")
def on_check_update(self):
"""Güncelleme kontrolünü başlat."""
def worker():
try:
self.message("Güncellemeler kontrol ediliyor…")
if hasattr(self, "update_manager") and self.update_manager:
# Farklı olası metod adlarını dene
for meth in ("check_for_updates", "check_updates", "check_update", "run_update_check"):
fn = getattr(self.update_manager, meth, None)
if callable(fn):
fn() # pylint: disable=not-callable
return
# Yedek: sürümler sayfasını aç
webbrowser.open("https://github.com/barkeser2002/turkanime-gui/releases")
self.message("Güncellemeler sayfasına yönlendirildi", error=False)
else:
self.message("Güncelleme yöneticisi bulunamadı", error=True)
except Exception as e:
self.message(f"Güncelleme kontrolü hatası: {e}", error=True)
threading.Thread(target=worker, daemon=True).start()
def check_for_updates_silent(self):
"""Sessiz güncelleme kontrolü - otomatik."""
def worker():
try:
if hasattr(self, "update_manager") and self.update_manager:
has_update, version_data = self.update_manager.check_for_updates(silent=True)
if has_update and version_data:
# Güncelleme varsa dialog göster
self.update_manager._show_update_dialog(version_data)
except Exception as e:
print(f"Otomatik güncelleme kontrolü hatası: {e}")
threading.Thread(target=worker, daemon=True).start()
def load_trending_anime(self):
"""Trend animeleri yükle - Jikan API (top airing) veya AniList'ten."""
def load_worker():
try:
# Önce Jikan API'den top airing animeleri dene
if JIKAN_AVAILABLE:
try:
trending = get_trending_anime_list(limit=12)
if trending and len(trending) > 0:
self.after(0, lambda: self.display_trending_anime(trending))
return
except Exception as jk_err:
print(f"[Jikan] Hata: {jk_err}")
# Jikan başarısız olduysa AniList'e düş
trending = anilist_client.get_trending_anime(page=1, per_page=12)
self.after(0, lambda: self.display_trending_anime(trending))
except Exception as e:
self.after(0, lambda: self.show_trending_error(str(e)))
threading.Thread(target=load_worker, daemon=True).start()
def display_trending_anime(self, anime_list):
"""Trend animeleri göster."""
# Loading label'ı kaldır
if hasattr(self, 'loading_label'):
self.loading_label.destroy()
# Grid oluştur - responsive sütun sayısı
row = 0
col = 0
max_cols = 6 # 6'dan 5'e düşürdük, kartlar daha büyük görünsün
for anime in anime_list[:12]: # 12'den 10'a düşürdük, daha az ama daha büyük kart
if col >= max_cols:
col = 0
row += 1
# Anime kartı
self.create_anime_card(self.trending_grid, anime, row, col, max_cols)
col += 1
def create_anime_card(self, parent, anime_data, row, col, max_cols):
"""Gelişmiş anime kartı oluştur - Netflix tarzı."""
# Daha büyük ve şık kart - genişlik artırıldı
card_width = 220 if max_cols == 5 else 200 # 5 sütun için daha geniş
card_frame = ctk.CTkFrame(parent, fg_color="#1a1a1a", width=card_width, height=340,
border_width=1, border_color="#333333",
corner_radius=12)
card_frame.grid(row=row, column=col, padx=10, pady=10, sticky="nw") # padding azaltıldı
card_frame.pack_propagate(False)
# Gelişmiş hover efekti
def on_enter(e):
card_frame.configure(fg_color="#2a2a2a", border_color="#ff6b6b")
def on_leave(e):
card_frame.configure(fg_color="#1a1a1a", border_color="#333333")
card_frame.bind("<Enter>", on_enter)
card_frame.bind("<Leave>", on_leave)
# Büyük kapak görseli - optimize edilmiş boyut
cover_width = 200 if max_cols == 5 else 180
cover_height = 260 if max_cols == 5 else 240
cover_frame = ctk.CTkFrame(card_frame, fg_color="#0f0f0f", width=cover_width, height=cover_height,
corner_radius=8)
cover_frame.pack(pady=(15, 0))
cover_frame.pack_propagate(False)
# Kapak görseli için optimize edilmiş alan
cover_label = ctk.CTkLabel(cover_frame, text="", font=ctk.CTkFont(size=60),
text_color="#666666")
cover_label.pack(expand=True)
# Kapak görselini yükle (optimize edilmiş boyut)
img_width = 180 if max_cols == 5 else 160
img_height = 240 if max_cols == 5 else 220
cover_url = anime_data.get('coverImage', {}).get('large')
if cover_url:
self.load_anilist_thumbnail(cover_url, cover_label, img_width, img_height)
# Başlık - geliştirilmiş format
title_text = anime_data.get('title', {}).get('romaji', 'Unknown')
# Daha akıllı başlık kısaltma
if len(title_text) > 22:
title_text = title_text[:19] + "..."
elif len(title_text) > 18:
title_text = title_text[:15] + "..."
wrap_width = 190 if max_cols == 5 else 170
title_label = ctk.CTkLabel(card_frame, text=title_text,
font=ctk.CTkFont(size=11, weight="bold"),
text_color="#ffffff", wraplength=wrap_width,
justify="center")
title_label.pack(pady=(10, 0))
# Skor ve bilgiler - yeniden düzenlenmiş
info_padding = 10 if max_cols == 5 else 8
info_frame = ctk.CTkFrame(card_frame, fg_color="transparent")
info_frame.pack(fill="x", padx=info_padding, pady=(6, 8))
# Skor - daha küçük ve şık
score = anime_data.get('averageScore', 0)
if score:
score_frame = ctk.CTkFrame(info_frame, fg_color="#ff6b6b", width=35, height=18,
corner_radius=8)
score_frame.pack(side="left")
score_frame.pack_propagate(False)
score_label = ctk.CTkLabel(score_frame, text=f"★{score}",
font=ctk.CTkFont(size=8, weight="bold"),
text_color="#ffffff")
score_label.pack()
# Popülerlik/Bölüm sayısı - daha küçük font
popularity = anime_data.get('episodes', 0)
if popularity:
ep_label = ctk.CTkLabel(info_frame, text=f"{popularity} bölüm",
font=ctk.CTkFont(size=8),
text_color="#cccccc")
ep_label.pack(side="right")
# Tıkla eventi - tüm alan tıklanabilir
def on_click():
self.show_anime_details(anime_data)
card_frame.bind("<Button-1>", lambda e: on_click())
cover_label.bind("<Button-1>", lambda e: on_click())
title_label.bind("<Button-1>", lambda e: on_click())
def show_anime_details(self, anime_data):
"""Anime detaylarını göster."""
# Race condition önleme: benzersiz request ID
import uuid as _uuid
request_id = str(_uuid.uuid4())
self._current_detail_request_id = request_id
with open("debug.log", "a") as f:
f.write(f"DEBUG: show_anime_details çağrıldı: {anime_data.get('title', {}).get('romaji', 'Unknown')} (rid={request_id[:8]})\n")
# Detay görünümü oluştur
self.clear_content_area()
# Seçili animeyi sakla (global oynat/indir butonları için)
self.selected_anime = anime_data
# Başlık selector'ünü güncelle
self.update_title_selector()
# Discord Rich Presence güncelle - AniList button'u ile
anime_title = anime_data.get('title', {}).get('romaji', 'Bilinmeyen Anime')
anilist_id = anime_data.get('id')
buttons = [
{
"label": "Uygulamayı Edin",
"url": "https://github.com/barkeser2002/turkanime-gui/releases"
}
]
if anilist_id:
buttons.append({
"label": "AniList'te Gör",
"url": f"https://anilist.co/anime/{anilist_id}"
})
self.update_discord_presence(f"{anime_title} detaylarına bakıyor", "TürkAnime GUI", buttons=buttons)
details_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
details_frame.pack(fill="both", expand=True)
# Geri butonu
back_btn = ctk.CTkButton(details_frame, text="← Geri",
command=self.show_home,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"))
back_btn.pack(anchor="nw", pady=(10, 20), padx=10)
# Ana içerik
content_frame = ctk.CTkFrame(details_frame, fg_color="#2a2a2a")
content_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20))
# Sol taraf - Kapak ve bilgiler
left_frame = ctk.CTkFrame(content_frame, fg_color="transparent")
left_frame.pack(side="left", fill="y", padx=(20, 40), pady=20)
# Büyük kapak
cover_frame = ctk.CTkFrame(left_frame, fg_color="#1a1a1a", width=250, height=350)
cover_frame.pack(pady=(0, 20))
cover_frame.pack_propagate(False)
cover_label = ctk.CTkLabel(cover_frame, text="", font=ctk.CTkFont(size=100))
cover_label.pack(expand=True)
cover_url = anime_data.get('coverImage', {}).get('large')
if cover_url:
self.load_anilist_thumbnail(cover_url, cover_label, 230, 330)
# Anime bilgileri
info_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
info_frame.pack(fill="x")
title = anime_data.get('title', {}).get('romaji', 'Unknown')
title_label = ctk.CTkLabel(info_frame, text=title,
font=ctk.CTkFont(size=24, weight="bold"),
text_color="#ffffff", wraplength=300)
title_label.pack(anchor="w", pady=(0, 10))
# Meta bilgiler
meta_info = []
if anime_data.get('episodes'):
meta_info.append(f"📺 {anime_data['episodes']} bölüm")
if anime_data.get('duration'):
meta_info.append(f"⏱️ {anime_data['duration']} dk")
if anime_data.get('season'):
meta_info.append(f"📅 {anime_data['season']} {anime_data.get('seasonYear', '')}")
if meta_info:
meta_label = ctk.CTkLabel(info_frame, text=" • ".join(meta_info),
font=ctk.CTkFont(size=12),
text_color="#cccccc")
meta_label.pack(anchor="w", pady=(0, 15))
# Skor ve popülerlik
stats_frame = ctk.CTkFrame(info_frame, fg_color="transparent")
stats_frame.pack(fill="x", pady=(0, 20))
score = anime_data.get('averageScore', 0)
if score:
score_frame = ctk.CTkFrame(stats_frame, fg_color="#333333", width=80, height=60)
score_frame.pack(side="left", padx=(0, 10))
score_frame.pack_propagate(False)
score_title = ctk.CTkLabel(score_frame, text="SKOR",
font=ctk.CTkFont(size=10),
text_color="#cccccc")
score_title.pack(pady=(5, 0))
score_value = ctk.CTkLabel(score_frame, text=f"{score}%",
font=ctk.CTkFont(size=18, weight="bold"),
text_color="#ffd93d")
score_value.pack()
popularity = anime_data.get('popularity', 0)
if popularity:
pop_frame = ctk.CTkFrame(stats_frame, fg_color="#333333", width=80, height=60)
pop_frame.pack(side="left", padx=(0, 10))
pop_frame.pack_propagate(False)
pop_title = ctk.CTkLabel(pop_frame, text="POPÜLER",
font=ctk.CTkFont(size=10),
text_color="#cccccc")
pop_title.pack(pady=(5, 0))
pop_value = ctk.CTkLabel(pop_frame, text=f"#{popularity}",
font=ctk.CTkFont(size=16, weight="bold"),
text_color="#74b9ff")
pop_value.pack()
# Aksiyon butonları
buttons_frame = ctk.CTkFrame(info_frame, fg_color="transparent")
buttons_frame.pack(fill="x", pady=(20, 0))
self.btnAddToList = ctk.CTkButton(buttons_frame, text="➕ Listeye Ekle",
command=lambda: self.add_anilist_to_library(anime_data),
fg_color="#ff6b6b", hover_color="#ff5252",
width=120, height=40)
self.btnAddToList.pack(side="left", padx=(0, 10))
self.btnSearchLocal = ctk.CTkButton(buttons_frame, text="🔍 Yerelde Ara",
command=lambda: self.search_anime_locally(title),
fg_color="#4ecdc4", hover_color="#45b7aa",
width=120, height=40)
self.btnSearchLocal.pack(side="left", padx=(0, 10))
# AniList sayfası butonu
anilist_id = anime_data.get('id')
if anilist_id:
self.btnAniListPage = ctk.CTkButton(buttons_frame, text="🌐 AniList",
command=lambda: self.open_anilist_page(anilist_id),
fg_color="#02a9ff", hover_color="#0099e5",
width=100, height=40)
self.btnAniListPage.pack(side="left", padx=(0, 10))
# Özel anime arama butonu - sadece bölüm bulunamadığında gösterilsin
self.btnCustomSearch = ctk.CTkButton(buttons_frame, text="🔍 İstediğin Anime Değil Mi?",
command=self.open_anime_search_dialog,
fg_color="transparent", border_width=2,
border_color="#ffd93d", text_color="#ffd93d",
width=180, height=40,
font=ctk.CTkFont(size=12, weight="bold"),
corner_radius=20)
# Başlangıçta gizli
self.btnCustomSearch.pack_forget()
# Sağ taraf - Özet ve detaylar
right_frame = ctk.CTkFrame(content_frame, fg_color="transparent")
right_frame.pack(side="right", fill="both", expand=True, padx=(0, 20), pady=20)
# Özet
summary_title = ctk.CTkLabel(right_frame, text="📖 Özet",
font=ctk.CTkFont(size=18, weight="bold"),
text_color="#ffffff")
summary_title.pack(anchor="w", pady=(0, 10))
description = anime_data.get('description') or 'Özet bulunamadı.'
# HTML taglerini temizle
import re
if description:
description = re.sub(r'<[^>]+>', '', description)
summary_textbox = ctk.CTkTextbox(right_frame, wrap="word", height=200)
summary_textbox.pack(fill="x", pady=(0, 20))
summary_textbox.insert("0.0", description)
summary_textbox.configure(state="disabled")
# Türler
if anime_data.get('genres'):
genres_title = ctk.CTkLabel(right_frame, text="🏷️ Türler",
font=ctk.CTkFont(size=16, weight="bold"),
text_color="#ffffff")
genres_title.pack(anchor="w", pady=(0, 10))
genres_text = ", ".join(anime_data['genres'])
genres_label = ctk.CTkLabel(right_frame, text=genres_text,
font=ctk.CTkFont(size=12),
text_color="#cccccc", wraplength=400)
genres_label.pack(anchor="w", pady=(0, 20))
# Stüdyolar - hem AniList (dict) hem LiveChart (list) formatını destekle
studios_data = anime_data.get('studios')
studios_list = []
if studios_data:
if isinstance(studios_data, dict) and studios_data.get('nodes'):
# AniList formatı: {'nodes': [{'name': 'Studio Name'}, ...]}
studios_list = [s['name'] for s in studios_data['nodes'] if s.get('name')]
elif isinstance(studios_data, list):
# LiveChart formatı: ['Studio Name', ...]
studios_list = [s for s in studios_data if isinstance(s, str)]
if studios_list:
studios_title = ctk.CTkLabel(right_frame, text="🎬 Stüdyo",
font=ctk.CTkFont(size=16, weight="bold"),
text_color="#ffffff")
studios_title.pack(anchor="w", pady=(0, 10))
studios_text = ", ".join(studios_list)
studios_label = ctk.CTkLabel(right_frame, text=studios_text,
font=ctk.CTkFont(size=12),
text_color="#cccccc")
studios_label.pack(anchor="w")
# Bölümler bölümü (Kaynaklar accordion içinde)
episodes_section = ctk.CTkFrame(details_frame, fg_color="transparent")
episodes_section.pack(fill="both", expand=True, padx=20, pady=(0, 20))
ep_title_row = ctk.CTkFrame(episodes_section, fg_color="transparent")
ep_title_row.pack(fill="x")
ep_title = ctk.CTkLabel(ep_title_row, text="",
font=ctk.CTkFont(size=18, weight="bold"),
text_color="#ffffff")
ep_title.pack(side="left")
# Toplu aksiyon butonları
bulk_actions = ctk.CTkFrame(ep_title_row, fg_color="transparent")
bulk_actions.pack(side="right")
btnPlayFirst = ctk.CTkButton(bulk_actions, text="▶️ İlk Seçiliyi Oynat",
width=160, height=34,
command=lambda: self._play_first_selected_episode())
btnPlayFirst.pack(side="left", padx=(0, 8))
btnDlSel = ctk.CTkButton(bulk_actions, text="⬇️ Seçilenleri İndir",
width=160, height=34,
command=lambda: self._download_selected_episodes())
btnDlSel.pack(side="left", padx=(0, 8))
btnSearchAgain = ctk.CTkButton(bulk_actions, text="🔍 İstediğin Anime Değil Mi?",
width=180, height=34,
command=self.open_anime_search_dialog)
btnSearchAgain.pack(side="left")
# Liste alanı
self.episodes_vars = [] # [(var, obj)]
self.episodes_objs = []
self.episodes_list = ctk.CTkFrame(episodes_section, fg_color="#1a1a1a")
self.episodes_list.pack(fill="both", expand=True, pady=(10, 0))
# Sonsuz scroll için değişkenler
self.episodes_loaded = 0
self.episodes_per_page = 20
self.is_loading_episodes = False
self.all_episodes = {} # Kaynak bazlı bölümler: {"AniList": [...], "TürkAnime": [...], "AnimeciX": [...]}
ep_loading = ctk.CTkLabel(self.episodes_list, text="Kaynaklar yükleniyor…",
font=ctk.CTkFont(size=14), text_color="#cccccc")
ep_loading.pack(pady=20)
# Başlık adayı: romaji -> english fallback
romaji = anime_data.get('title', {}).get('romaji') or ""
english = anime_data.get('title', {}).get('english') or ""
query_title = romaji if romaji else english
# DB'den önceki eşleşmeleri kontrol et
db_matches = self._load_anime_matches_from_db(query_title)
def load_sources_worker():
"""Kaynakları yükle - SearchEngine ile ve timeout atlama ile."""
with open("debug.log", "a") as f:
f.write("DEBUG: load_sources_worker başladı\n")
try:
# Tüm kaynaklarda paralel arama yap
sources_data = {}
source_status = {} # Her kaynağın durumunu takip et
source_timeouts = {
"TürkAnime": 8, # 8 saniye
"AnimeciX": 8, # 8 saniye
"Anizle": 8, # 8 saniye
"TRAnimeİzle": 12, # 12 saniye (bot koruma)
"AniList": 5 # 5 saniye (daha hızlı)
}
def search_source_with_timeout(source_name, timeout_seconds):
"""Kaynak arama işlemi - timeout ile ve hata loglaması ile."""
def timeout_handler():
"""Timeout durumunda çağrılır."""
if source_status.get(source_name) == "loading":
source_status[source_name] = "timeout"
sources_data[source_name] = []
with open("debug.log", "a") as f:
f.write(f"ERROR: {source_name} timeout ({timeout_seconds}s) - kaynak atlandı\n")
# Timeout timer'ı başlat
timer = threading.Timer(timeout_seconds, timeout_handler)
timer.start()
try:
source_status[source_name] = "loading"
start_time = time.time()
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} araması başlatılıyor (timeout: {timeout_seconds}s)\n")
if source_name == "AniList":
# AniList'te ara (sadece bilgi amaçlı)
results = anilist_client.search_anime(query_title, per_page=1)
if results:
anime_data = results[0]
sources_data[source_name] = [{"title": anime_data.get('title', {}).get('romaji'), "obj": anime_data, "anime_title": anime_data.get('title', {}).get('romaji')}]
source_status[source_name] = "completed"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} tamamlandı - {len(sources_data[source_name])} sonuç\n")
else:
source_status[source_name] = "no_results"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} - sonuç bulunamadı\n")
elif source_name == "TürkAnime":
# SearchEngine kullanarak TürkAnime'de ara
adapter = search_engine.get_adapter(source_name)
if adapter:
search_results = adapter.search_anime(query_title, limit=1)
if search_results:
slug, name = search_results[0]
# Anime objesi oluştur ve bölümleri al
ani = Anime(slug)
episodes = []
for b in ani.bolumler:
episodes.append({
"title": b.title,
"obj": b,
"anime_title": name
})
sources_data[source_name] = episodes
source_status[source_name] = "completed"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} tamamlandı - {len(episodes)} bölüm\n")
else:
source_status[source_name] = "no_results"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} - sonuç bulunamadı\n")
else:
source_status[source_name] = "error"
with open("debug.log", "a") as f:
f.write(f"ERROR: {source_name} adapter bulunamadı\n")
elif source_name == "AnimeciX":
# SearchEngine kullanarak AnimeciX'te ara
adapter = search_engine.get_adapter(source_name)
if adapter:
search_results = adapter.search_anime(query_title, limit=1)
if search_results:
_id, name = search_results[0]
# _id'yi güvenli şekilde int'e çevir
try:
safe_id = int(_id)
except (ValueError, TypeError):
safe_id = hash(str(_id)) % 1000000
cix = CixAnime(id=str(safe_id), title=str(name))
eps = cix.episodes
episodes = []
ada = AdapterAnime(slug=str(cix.id), title=cix.title)
for e in eps:
ab = AdapterBolum(url=e.url, title=e.title, anime=ada)
episodes.append({
"title": e.title,
"obj": ab,
"anime_title": name
})
sources_data[source_name] = episodes
source_status[source_name] = "completed"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} tamamlandı - {len(episodes)} bölüm\n")
else:
source_status[source_name] = "no_results"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} - sonuç bulunamadı\n")
else:
source_status[source_name] = "error"
with open("debug.log", "a") as f:
f.write(f"ERROR: {source_name} adapter bulunamadı\n")
elif source_name == "Anizle":
# SearchEngine kullanarak Anizle'de ara
adapter = search_engine.get_adapter(source_name)
if adapter:
search_results = adapter.search_anime(query_title, limit=1)
if search_results:
slug, name = search_results[0]
anizle_anime = AnizleAnime(slug=slug, title=name)
eps = anizle_anime.episodes
episodes = []
ada = AdapterAnime(slug=anizle_anime.slug, title=anizle_anime.title)
# stream_provider sadece url parametresi almalı
stream_provider = lambda url: get_episode_streams(url, timeout=10)
for e in eps:
ab = AdapterBolum(
url=e.url,
title=e.title,
anime=ada,
stream_provider=stream_provider,
player_name="ANIZLE"
)
episodes.append({
"title": e.title,
"obj": ab,
"anime_title": name
})
sources_data[source_name] = episodes
source_status[source_name] = "completed"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} tamamlandı - {len(episodes)} bölüm\n")
else:
source_status[source_name] = "no_results"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} - sonuç bulunamadı\n")
else:
source_status[source_name] = "error"
with open("debug.log", "a") as f:
f.write(f"ERROR: {source_name} adapter bulunamadı\n")
elif source_name == "TRAnimeİzle":
# TRAnimeİzle'de ara
try:
search_results = search_tranime(query_title, limit=1)
if search_results:
slug, name = search_results[0]
# TRAnimeİzle bölümlerini al
tranime_episodes = get_tranime_episodes(slug)
episodes = []
ada = AdapterAnime(slug=slug, title=name)
for e in tranime_episodes:
# Stream provider: bölüm detaylarını alıp video linkini çıkarır
def make_stream_provider(ep_slug):
def provider(url):
try:
ep_details = get_tranime_episode_details(ep_slug)
if ep_details:
sources = ep_details.get_sources()
streams = []
for s in sources:
iframe = s.get_iframe()
if iframe:
streams.append({"url": iframe, "label": s.name})
return streams
except Exception:
pass
return []
return provider
ab = AdapterBolum(
url=e.slug, # Bölüm slug'ı
title=e.title,
anime=ada,
stream_provider=make_stream_provider(e.slug),
player_name="TRANIME"
)
episodes.append({
"title": e.title,
"obj": ab,
"anime_title": name
})
sources_data[source_name] = episodes
source_status[source_name] = "completed"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} tamamlandı - {len(episodes)} bölüm\n")
else:
source_status[source_name] = "no_results"
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} - sonuç bulunamadı\n")
except Exception as e:
source_status[source_name] = "error"
with open("debug.log", "a") as f:
f.write(f"ERROR: {source_name} hatası: {str(e)}\n")
# Başarılı tamamlandı, timer'ı iptal et
timer.cancel()
elapsed_time = time.time() - start_time
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} tamamlandı: {elapsed_time:.2f}s\n")
except Exception as e:
# Timer'ı iptal et
timer.cancel()
elapsed_time = time.time() - start_time if 'start_time' in locals() else 0
with open("debug.log", "a") as f:
f.write(f"ERROR: {source_name} arama hatası ({elapsed_time:.2f}s): {str(e)}\n")
sources_data[source_name] = []
source_status[source_name] = "error"
# Paralel arama - her kaynak için ayrı thread
threads = []
all_sources = ["TürkAnime", "AnimeciX", "AniList", "Anizle", "TRAnimeİzle"]
for source_name in all_sources:
timeout = source_timeouts.get(source_name, 10)
thread = threading.Thread(
target=search_source_with_timeout,
args=(source_name, timeout),
daemon=True
)
threads.append(thread)
thread.start()
# Tüm thread'lerin bitmesini bekle (maksimum toplam süre)
total_timeout = 18 # Toplam maksimum 18 saniye (TRAnimeİzle için arttırıldı)
start_total = time.time()
for i, thread in enumerate(threads):
source_name = all_sources[i]
# Eğer bu kaynak zaten timeout olduysa, bekleme
if source_status.get(source_name) == "timeout":
with open("debug.log", "a") as f:
f.write(f"DEBUG: {source_name} zaten timeout oldu, beklenmiyor\n")
continue
remaining_time = total_timeout - (time.time() - start_total)
if remaining_time > 0:
thread.join(timeout=remaining_time)
# Thread hala çalışıyorsa ve timeout olduysa işaretle
if thread.is_alive() and source_status.get(source_name) != "completed":
source_status[source_name] = "timeout"
sources_data[source_name] = []
with open("debug.log", "a") as f:
f.write(f"ERROR: {source_name} thread timeout - kaynak atlandı\n")
else:
# Toplam timeout aşıldı
if source_status.get(source_name) != "completed":
source_status[source_name] = "timeout"
sources_data[source_name] = []
with open("debug.log", "a") as f:
f.write(f"ERROR: Toplam timeout ({total_timeout}s) aşıldı, {source_name} atlandı\n")
# Hangi kaynakların tamamlandığını kontrol et ve logla
completed_sources = [s for s, status in source_status.items() if status == "completed"]
failed_sources = [s for s, status in source_status.items() if status in ["error", "timeout"]]
no_results_sources = [s for s, status in source_status.items() if status == "no_results"]
with open("debug.log", "a") as f:
if completed_sources:
f.write(f"INFO: Başarıyla yüklenen kaynaklar: {', '.join(completed_sources)}\n")
if failed_sources:
f.write(f"ERROR: Başarısız olan kaynaklar: {', '.join(failed_sources)}\n")
if no_results_sources:
f.write(f"INFO: Sonuç bulunmayan kaynaklar: {', '.join(no_results_sources)}\n")
# Bölümleri sakla ve render et — sadece bu request hala aktifse
if getattr(self, '_current_detail_request_id', None) != request_id:
with open("debug.log", "a") as f:
f.write(f"WARN: load_sources_worker tamamlandı ama request_id değişti, render iptal (rid={request_id[:8]})\n")
return
self.all_episodes = sources_data
with open("debug.log", "a") as f:
f.write(f"DEBUG: all_episodes set to: {list(sources_data.keys())}\n")
self.after(0, lambda: self._update_loading_status("Kaynaklar işleniyor..."))
self.after(100, lambda: self.render_sources_page(db_matches) if getattr(self, '_current_detail_request_id', None) == request_id else None)
with open("debug.log", "a") as f:
f.write("DEBUG: render_sources_page scheduled\n")
except Exception as e:
with open("debug.log", "a") as f:
f.write(f"ERROR: load_sources_worker genel hata: {str(e)}\n")
import traceback
traceback.print_exc()
# Hata durumunda boş veri ile devam et
if getattr(self, '_current_detail_request_id', None) == request_id:
self.all_episodes = {}
self.after(0, lambda: self.render_sources_page(db_matches))
# Kaynak yükleme worker'ını başlat
with open("debug.log", "a") as f:
f.write("DEBUG: Thread başlatılıyor\n")
worker_thread = threading.Thread(target=load_sources_worker, daemon=True)
worker_thread.start()
with open("debug.log", "a") as f:
f.write("DEBUG: Thread başlatıldı\n")
def _update_loading_status(self, message):
"""Loading durumunu güncelle."""
try:
# Loading label'ını bul ve güncelle
if self.episodes_list:
for widget in self.episodes_list.winfo_children():
if hasattr(widget, 'configure') and hasattr(widget, 'cget'):
try:
current_text = widget.cget('text')
if "yükleniyor" in current_text.lower() or "işleniyor" in current_text.lower():
widget.configure(text=message)
break
except:
continue
except Exception as e:
print(f"Loading status güncelleme hatası: {e}")
with open("debug.log", "a") as f:
f.write("DEBUG: show_anime_details tamamlandı\n")
def _check_and_correct_anime_name(self):
"""Anime adını API'den kontrol et ve gerekirse düzelt.
Sadece benzer isimler arasında düzeltme yapar.
Tamamen farklı animeleri düzeltmez.
Alternatif başlıkları (İngilizce, Japonca, Romaji) da karşılaştırır.
"""
if not hasattr(self, 'selected_anime') or not self.selected_anime:
return
try:
from turkanime_api.common.db import api_manager
from difflib import SequenceMatcher
def similarity(a: str, b: str) -> float:
"""İki string arasındaki benzerlik oranını hesapla (0-1)."""
if not a or not b:
return 0.0
a_lower = a.lower().strip()
b_lower = b.lower().strip()
return SequenceMatcher(None, a_lower, b_lower).ratio()
def best_similarity_for_title(candidate: str, titles: list) -> float:
"""Bir adayı tüm alternatif başlıklara karşı en yüksek benzerlikle eşleştir."""
return max((similarity(candidate, t) for t in titles if t), default=0.0)
# Mevcut anime adları (tüm alternatifler)
title_data = self.selected_anime.get('title', {})
romaji = title_data.get('romaji', '') or ''
english = title_data.get('english', '') or ''
native = title_data.get('native', '') or ''
all_titles = [t for t in [romaji, english, native] if t]
# Ana başlık
current_name = romaji or english or native
if not current_name:
return
# API'den bu anime için eşleştirmeleri ara (search_anime_matches kullan!)
matches = api_manager.search_anime_matches(current_name)
if not matches:
return
# Sadece benzer isimleri say (benzerlik > 0.4 — düşürüldü)
SIMILARITY_THRESHOLD = 0.4
name_counts: dict = {}
for match in matches:
anime_title = match.get('anime_title', '')
if anime_title:
sim = best_similarity_for_title(anime_title, all_titles)
if sim >= SIMILARITY_THRESHOLD:
# Benzerlik skoru ile birlikte say
if anime_title not in name_counts:
name_counts[anime_title] = {'count': 0, 'similarity': sim}
name_counts[anime_title]['count'] += 1
if not name_counts:
return
# En yüksek (benzerlik * sayı) skoruna sahip olanı seç
best_match = max(
name_counts.items(),
key=lambda x: x[1]['similarity'] * x[1]['count']
)
correct_name = best_match[0]
best_similarity = best_match[1]['similarity']
# Eğer farklıysa ve benzerlik yeterince yüksekse güncelle
if correct_name != current_name and best_similarity >= 0.6:
print(f"Anime adı otomatik düzeltildi: {current_name} -> {correct_name} (benzerlik: {best_similarity:.2f})")
# Anime adını güncelle
if 'title' not in self.selected_anime:
self.selected_anime['title'] = {}
self.selected_anime['title']['romaji'] = correct_name
self.selected_anime['title']['english'] = correct_name
self.selected_anime['title']['native'] = correct_name
# Kullanıcıya bilgi ver
self.message(f"Anime adı düzeltildi: {correct_name}", error=False)
except Exception as e:
print(f"Anime adı kontrolü hatası: {e}")
def render_sources_page(self, db_matches=None):
"""Kaynakları accordion tarzında göster - performans iyileştirmesi ile."""
with open("debug.log", "a") as f:
f.write(f"DEBUG: render_sources_page çağrıldı, all_episodes: {list(self.all_episodes.keys()) if self.all_episodes else 'None'}\n")
# Anime adı kontrolü — arka planda yap (UI'yı bloke etmesin)
def _bg_name_check():
try:
self._check_and_correct_anime_name()
except Exception:
pass
threading.Thread(target=_bg_name_check, daemon=True).start()
# Discord Rich Presence güncelle
if hasattr(self, 'selected_anime') and self.selected_anime:
anime_title = self.selected_anime.get('title', {}).get('romaji', 'Bilinmeyen Anime')
self.update_discord_presence(f"{anime_title} kaynaklarına bakıyor", "TürkAnime GUI")
try:
# Loading label'ı kaldır
if self.episodes_list:
for widget in self.episodes_list.winfo_children():
if hasattr(widget, 'cget') and widget.cget('text') == "Kaynaklar yükleniyor…":
widget.destroy()
break
except:
pass
if not self.all_episodes or not any(episodes for episodes in self.all_episodes.values()):
# Bölüm bulunamadı - koşullu butonu göster
if hasattr(self, 'btnCustomSearch'):
try:
self.btnCustomSearch.pack(side="left")
except:
pass
# Bölüm bulunamadı mesajı
not_found_frame = ctk.CTkFrame(self.episodes_list, fg_color="transparent")
not_found_frame.pack(fill="x", padx=10, pady=10)
not_found_label = ctk.CTkLabel(not_found_frame,
text="Hiçbir kaynakta bölüm bulunamadı",
text_color="#ff6b6b")
not_found_label.pack(pady=(0, 10))
return
# Sadece bölümler için kullanılacak kaynakları ayır (AniList hariç)
display_sources = {k: v for k, v in self.all_episodes.items() if k in ["TürkAnime", "AnimeciX", "Anizle", "TRAnimeİzle"]}
# Kaynak durumlarını kontrol et ve kullanıcıya bildir
loaded_sources = [k for k, v in display_sources.items() if v and len(v) > 0]
failed_sources = [k for k in ["TürkAnime", "AnimeciX", "Anizle", "TRAnimeİzle"] if k not in display_sources or not display_sources.get(k)]
if loaded_sources:
status_msg = f"✅ {len(loaded_sources)} kaynak yüklendi: {', '.join(loaded_sources)}"
if failed_sources:
status_msg += f"\n⚠️ {len(failed_sources)} kaynak yüklenemedi: {', '.join(failed_sources)}"
self.message(status_msg, error=False)
elif failed_sources:
self.message(f"❌ Tüm kaynaklar yüklenemedi: {', '.join(failed_sources)}", error=True)
# İlk adım: Loading mesajı göster
if self.episodes_list:
loading_frame = ctk.CTkFrame(self.episodes_list, fg_color="transparent")
loading_frame.pack(fill="x", padx=10, pady=10)
loading_label = ctk.CTkLabel(loading_frame, text="Bölümler hazırlanıyor...",
font=ctk.CTkFont(size=14), text_color="#cccccc")
loading_label.pack(pady=20)
# UI güncellemesi için update_idletasks kullan
self.update_idletasks()
def create_accordion():
try:
with open("debug.log", "a") as f:
f.write("DEBUG: create_accordion başladı\n")
# Loading frame'i kaldır
loading_frame.destroy()
# AccordionSourceEpisodeList ile kaynakları göster (sadece bölümler için)
from turkanime_api.common.ui import AccordionSourceEpisodeList
# Anime adını al
anime_name = "unknown"
if hasattr(self, 'selected_anime') and self.selected_anime:
anime_name = self.selected_anime.get('title', {}).get('romaji',
self.selected_anime.get('title', {}).get('english',
self.selected_anime.get('title', {}).get('native', 'unknown')))
self.source_accordion = AccordionSourceEpisodeList(
self.episodes_list, display_sources, max_episodes_per_source=50,
on_play=self._play_episode, on_download=self._download_episode,
on_match=self._handle_anime_match, db_matches=db_matches,
user_id=self.dosya.ayarlar.get('user_id'), anime_name=anime_name,
main_window=self
)
# Bölüm bulunduğunda koşullu butonu gizle
if hasattr(self, 'btnCustomSearch'):
try:
self.btnCustomSearch.pack_forget()
except:
pass
# UI güncellemesi için update_idletasks kullan
self.update_idletasks()
with open("debug.log", "a") as f:
f.write("DEBUG: create_accordion tamamlandı\n")
except Exception as e:
with open("debug.log", "a") as f:
f.write(f"ERROR: create_accordion hatası: {str(e)}\n")
print(f"Accordion oluşturma hatası: {e}")
# Hata durumunda loading label'ını güncelle
try:
loading_label.configure(text=f"Hata: {e}", text_color="#ff6b6b")
except:
pass
# Kısa bir bekleme sonrası accordion'u oluştur (performans için)
self.after(50, create_accordion) # Bu çağrı render_sources_page içinde yapılıyor
with open("debug.log", "a") as f:
f.write("DEBUG: render_sources_page tamamlandı\n")
def _handle_anime_match(self, selected_anime):
"""Anime eşleştirmesini işle ve DB'ye kaydet."""
try:
print("DEBUG: _handle_anime_match başladı")
# Geçerli seçimleri kontrol et
valid_selections = {}
for source, anime_title in selected_anime.items():
if anime_title and anime_title not in ["Arama yapın...", "Sonuç bulunamadı", "Arama hatası"]:
# DB'den gelen eşleşmeler için prefix'i kaldır
clean_title = anime_title.replace("📚 ", "") if anime_title.startswith("📚 ") else anime_title
valid_selections[source] = clean_title
print(f"DEBUG: selected_anime = {selected_anime}")
print(f"DEBUG: valid_selections = {valid_selections}")
print(f"DEBUG: len(valid_selections) = {len(valid_selections)}")
if not valid_selections or len(valid_selections) < 2:
self.message("Tüm kaynaklardan geçerli anime seçmelisiniz!", error=True)
return
# Eşleşen anime'leri DB'ye kaydet
from turkanime_api.common.db import api_manager
saved_count = 0
for source, anime_title in selected_anime.items():
try:
# Anime ID'sini bul (mevcut anime'den)
anime_id = None
if source in self.all_episodes and self.all_episodes[source]:
# İlk bölümün objesinden anime ID'sini al
first_episode = self.all_episodes[source][0]
if hasattr(first_episode['obj'], 'anime'):
anime_id = first_episode['obj'].anime.slug
if anime_id:
success = api_manager.save_anime_match(source, str(anime_id), anime_title)
if success:
saved_count += 1
except Exception as e:
print(f"{source} eşleştirme hatası: {e}")
if saved_count > 0:
self.message(f"{saved_count} kaynak eşleştirildi ve kaydedildi!", error=False)
# Anime adını güncelle (eğer farklıysa)
if hasattr(self, 'selected_anime') and self.selected_anime:
# Tüm kaynaklardan aynı anime adını kullan (ilk geçerli olanı)
correct_anime_name = None
for source, anime_title in valid_selections.items():
if anime_title and anime_title not in ["Arama yapın...", "Sonuç bulunamadı", "Arama hatası"]:
correct_anime_name = anime_title
break
if correct_anime_name:
# Mevcut anime adını kontrol et ve güncelle
current_name = self.selected_anime.get('title', {}).get('romaji', '') or \
self.selected_anime.get('title', {}).get('english', '') or \
self.selected_anime.get('title', {}).get('native', '')
if current_name != correct_anime_name:
print(f"Anime adı güncellendi: {current_name} -> {correct_anime_name}")
# Anime adını güncelle (title alanını değiştir)
if 'title' not in self.selected_anime:
self.selected_anime['title'] = {}
self.selected_anime['title']['romaji'] = correct_anime_name
self.selected_anime['title']['english'] = correct_anime_name
self.selected_anime['title']['native'] = correct_anime_name
else:
self.message("Eşleştirme kaydedilemedi!", error=True)
except Exception as e:
print(f"DEBUG: _handle_anime_match exception: {e}")
import traceback
traceback.print_exc()
self.message(f"Eşleştirme hatası: {e}", error=True)
def _load_anime_matches_from_db(self, anime_title: str) -> Dict[str, Dict]:
"""DB'den anime eşleşmelerini yükle."""
from turkanime_api.common.db import api_manager
matches = {}
try:
# Tüm eşleşmeleri al (şimdilik basit yaklaşım)
# Gerçek uygulamada anime_title ile filtreleme yapılabilir
all_matches = api_manager.get_anime_matches()
for match in all_matches:
source = match['source']
# Basit eşleştirme: anime_title ile karşılaştır
if anime_title.lower() in match['anime_title'].lower():
matches[source] = {
'anime_id': match['anime_id'],
'anime_title': match['anime_title']
}
except Exception as e:
print(f"DB eşleşme yükleme hatası: {e}")
return matches
# --- Bölüm oynatma/indirme yardımcıları ---
def _play_episode(self, episode_obj):
def worker():
try:
vf = VideoFindWorker(episode_obj)
vf.signals.connect_found(self.play_video)
vf.signals.connect_error(lambda msg: self.message(f"Hata: {msg}", error=True))
vf.run()
except Exception as e:
self.message(f"Video arama hatası: {e}", error=True)
threading.Thread(target=worker, daemon=True).start()
def _download_episode(self, episode_obj):
def worker():
try:
# Discord Rich Presence güncelle
if episode_obj and episode_obj.anime:
anime_title = episode_obj.anime.title
self.update_discord_presence_download(anime_title, "0")
dw = DownloadWorker([episode_obj])
self._active_download_worker = dw
# İndirme panelini göster
self.after(0, lambda: self._show_download_panel([episode_obj]))
# Progress bar güncelleme
def on_progress_item(data):
pct = data.get("percent")
status = data.get("status", "")
slug = data.get("slug", "")
speed = data.get("speed")
# Hız formatlama
speed_str = ""
if speed:
if speed > 1024 * 1024:
speed_str = f" ({speed / 1024 / 1024:.1f} MB/s)"
elif speed > 1024:
speed_str = f" ({speed / 1024:.0f} KB/s)"
if pct is not None:
self.after(0, lambda: self._update_download_progress(
slug, pct / 100.0, f"{status} %{pct}{speed_str}"))
else:
self.after(0, lambda: self._update_download_progress(
slug, 0, status))
dw.signals.connect_progress(lambda msg: self.after(0, lambda m=msg: self.status_label.configure(text=m) if hasattr(self, 'status_label') else None))
dw.signals.connect_progress_item(on_progress_item)
dw.signals.connect_success(lambda: self.after(0, lambda: self._on_download_complete("İndirme tamamlandı!")))
dw.signals.connect_error(lambda msg: self.after(0, lambda m=msg: self.message(f"İndirme hatası: {m}", error=True)))
dw.run()
except Exception as e:
self.after(0, lambda: self.message(f"İndirme başlatılamadı: {e}", error=True))
threading.Thread(target=worker, daemon=True).start()
def _show_download_panel(self, bolumler):
"""İndirme panelini göster - her bölüm için progress bar."""
if not hasattr(self, '_download_panel') or self._download_panel is None:
self._download_panel = {}
# Mevcut paneli temizle
if hasattr(self, '_download_frame') and self._download_frame:
try:
self._download_frame.destroy()
except Exception:
pass
# content_area'nın en altına panel ekle
parent = self.content_area if hasattr(self, 'content_area') else self
self._download_frame = ctk.CTkFrame(parent, fg_color="#1a1a2e", corner_radius=12)
self._download_frame.pack(fill="x", padx=10, pady=(5, 10))
# Başlık satırı
header_frame = ctk.CTkFrame(self._download_frame, fg_color="#16213e", corner_radius=8, height=40)
header_frame.pack(fill="x", padx=3, pady=3)
header_frame.pack_propagate(False)
ctk.CTkLabel(header_frame, text=f"⬇️ İndirme Kuyruğu ({len(bolumler)} bölüm)",
font=ctk.CTkFont(size=13, weight="bold"),
text_color="#e94560").pack(side="left", padx=15, pady=8)
# İptal butonu
cancel_btn = ctk.CTkButton(header_frame, text="✕ İptal", width=70, height=28,
fg_color="#c0392b", hover_color="#e74c3c",
font=ctk.CTkFont(size=11),
command=self._cancel_downloads)
cancel_btn.pack(side="right", padx=10, pady=6)
# İndirme satırları
items_frame = ctk.CTkFrame(self._download_frame, fg_color="transparent")
items_frame.pack(fill="x", padx=10, pady=(5, 10))
for bolum in bolumler:
slug = bolum.slug
row = ctk.CTkFrame(items_frame, fg_color="#0f3460", corner_radius=6, height=50)
row.pack(fill="x", pady=2)
row.pack_propagate(False)
# Sol: başlık
title_label = ctk.CTkLabel(row, text=f"📄 {bolum.title}",
font=ctk.CTkFont(size=11),
text_color="#e0e0e0", anchor="w")
title_label.pack(side="left", padx=10, fill="x", expand=True)
# Sağ taraf: durum + progress bar
right_frame = ctk.CTkFrame(row, fg_color="transparent", width=250)
right_frame.pack(side="right", padx=10)
right_frame.pack_propagate(False)
progress_bar = ctk.CTkProgressBar(right_frame, width=150, height=12,
progress_color="#e94560",
fg_color="#1a1a2e")
progress_bar.pack(side="left", padx=(0, 8), pady=15)
progress_bar.set(0)
status_label = ctk.CTkLabel(right_frame, text="bekliyor",
font=ctk.CTkFont(size=10),
text_color="#999999", width=80)
status_label.pack(side="left")
self._download_panel[slug] = {
"progress_bar": progress_bar,
"status_label": status_label,
"title_label": title_label,
}
def _update_download_progress(self, slug, value, status_text):
"""İndirme panelindeki bir öğeyi güncelle."""
if not hasattr(self, '_download_panel') or not self._download_panel:
return
item = self._download_panel.get(slug)
if not item:
return
try:
bar = item.get("progress_bar")
lbl = item.get("status_label")
title_lbl = item.get("title_label")
if bar and value is not None:
bar.set(min(1.0, max(0, value)))
if lbl and status_text:
lbl.configure(text=status_text[:30])
# Renge göre güncelle
if "tamamlandı" in status_text or "indirildi" in status_text:
lbl.configure(text_color="#2ecc71")
if bar:
bar.configure(progress_color="#2ecc71")
elif "hata" in status_text:
lbl.configure(text_color="#e74c3c")
if bar:
bar.configure(progress_color="#e74c3c")
elif "indiriliyor" in status_text:
lbl.configure(text_color="#3498db")
elif "tekrar" in status_text:
lbl.configure(text_color="#f39c12")
else:
lbl.configure(text_color="#999999")
except Exception:
pass
def _on_download_complete(self, msg):
"""İndirme tamamlandığında çağrılır."""
self.message(msg)
# Panel'i 5 saniye sonra temizle
if hasattr(self, '_download_frame') and self._download_frame:
self.after(5000, self._cleanup_download_panel)
def _cancel_downloads(self):
"""Aktif indirmeyi iptal et."""
if hasattr(self, '_active_download_worker') and self._active_download_worker:
self._active_download_worker.cancel()
self.message("⚠️ İndirme iptal ediliyor...", error=False)
def _cleanup_download_panel(self):
"""İndirme panelini temizle."""
try:
if hasattr(self, '_download_frame') and self._download_frame:
self._download_frame.destroy()
self._download_frame = None
self._download_panel = {}
self._active_download_worker = None
except Exception:
pass
def _play_first_selected_episode(self):
"""İlk seçili bölümü oynat."""
selected = []
# Önce source_accordion'dan seçili bölümleri al
if hasattr(self, 'source_accordion') and self.source_accordion:
selected = self.source_accordion.get_selected_episodes()
# Eğer source_accordion'dan alınamadıysa episodes_vars'tan dene
if not selected and getattr(self, 'episodes_vars', None):
for var, obj in self.episodes_vars:
if var.get():
selected.append(obj)
break
if selected:
self._play_episode(selected[0])
else:
self.message("❌ Önce bölüm seçin (checkbox'ları işaretleyin)", error=True)
def _search_anime_again(self):
"""Yeni arama penceresi aç ve anime eşleştir."""
if not hasattr(self, 'selected_anime') or not self.selected_anime:
self.message("Önce bir anime seçin", error=True)
return
# Mevcut anime bilgilerini al
anime_data = self.selected_anime
romaji = anime_data.get('title', {}).get('romaji') or ""
english = anime_data.get('title', {}).get('english') or ""
original_title = romaji if romaji else english
# Discord Rich Presence güncelle
self.update_discord_presence(f"'{original_title}' arıyor", "TürkAnime GUI")
# Jikan (MAL) ile ara, başarısız olursa AniList'e fallback
self.message("Jikan (MAL) ile aranıyor…")
def search_worker():
try:
results = None
if JIKAN_AVAILABLE and jikan_search_anime:
try:
results = jikan_search_anime(original_title, limit=20)
except Exception:
results = None
if not results:
results = anilist_client.search_anime(original_title)
self.after(0, lambda: self.display_anilist_search_results(results, f"Arama: {original_title}"))
except Exception as e:
self.after(0, lambda: self.message(f"Arama hatası: {e}", error=True))
threading.Thread(target=search_worker, daemon=True).start()
def _download_selected_episodes(self):
"""Seçili bölümleri indir - kaynak seçimi ile."""
if not hasattr(self, 'source_accordion') or not self.source_accordion:
self.message("❌ Bölüm listesi bulunamadı!", error=True)
return
# Seçili bölüm var mı kontrol et
if not self.source_accordion.selected_episodes:
self.message("❌ İndirilecek bölüm seçilmedi! Checkbox'ları işaretleyin.", error=True)
return
# Kaynak seçim dialogunu göster
def start_download(source_name: str):
"""Seçilen kaynaktan indirmeyi başlat."""
selected = self.source_accordion.get_selected_episodes(source_name) # type: ignore[union-attr]
if not selected:
self.message(f"❌ {source_name} kaynağında seçili bölüm bulunamadı!", error=True)
return
# İndirme başlatıldığını göster
self.message(f"⬇️ {len(selected)} bölüm {source_name}'dan indiriliyor...", error=False)
def worker():
try:
# Discord Rich Presence güncelle
if selected and selected[0].anime:
anime_title = selected[0].anime.title
self.update_discord_presence_download(anime_title, "0")
dw = DownloadWorker(selected, update_callback=self.update_downloaded_list)
self._active_download_worker = dw
# İndirme panelini göster
self.after(0, lambda: self._show_download_panel(selected))
def on_progress_item(data):
pct = data.get("percent")
status = data.get("status", "")
slug = data.get("slug", "")
speed = data.get("speed")
error = data.get("error")
speed_str = ""
if speed:
if speed > 1024 * 1024:
speed_str = f" ({speed / 1024 / 1024:.1f} MB/s)"
elif speed > 1024:
speed_str = f" ({speed / 1024:.0f} KB/s)"
status_text = status
if error:
status_text = f"hata: {error[:50]}"
elif pct is not None:
status_text = f"{status} %{pct}{speed_str}"
if pct is not None:
self.after(0, lambda: self._update_download_progress(
slug, pct / 100.0, status_text))
else:
self.after(0, lambda: self._update_download_progress(
slug, 0, status_text))
dw.signals.connect_progress(lambda msg: self.after(0, lambda m=msg: self.status_label.configure(text=m) if hasattr(self, 'status_label') else None))
dw.signals.connect_progress_item(on_progress_item)
dw.signals.connect_success(lambda: self.after(0, lambda: self._on_download_complete(f"✅ {source_name}'dan {len(selected)} bölüm indirildi!")))
dw.signals.connect_error(lambda msg: self.after(0, lambda m=msg: self.message(f"❌ İndirme hatası: {m}", error=True)))
dw.run()
except Exception as e:
self.after(0, lambda: self.message(f"❌ İndirme başlatılamadı: {e}", error=True))
threading.Thread(target=worker, daemon=True).start()
# Kaynak seçim dialogunu göster
self.source_accordion.show_source_selection_dialog(start_download)
def clear_content_area(self):
"""İçerik alanını temizle."""
for widget in self.content_area.winfo_children():
widget.destroy()
def show_home(self):
"""Ana sayfayı göster."""
self.current_view = "home"
self.clear_content_area()
# Navigasyon butonlarını güncelle
self._update_nav_buttons("home")
# Discord Rich Presence güncelle
self.update_discord_presence("Ana sayfada", "TürkAnime GUI")
# Başlık selector'ünü güncelle
self.update_title_selector()
self.create_home_content()
def show_trending(self):
"""Trend sayfası göster - AniList trending."""
self.current_view = "trending"
self.clear_content_area()
# Navigasyon butonlarını güncelle
self._update_nav_buttons("trending")
# Discord Rich Presence güncelle
self.update_discord_presence("Trend animelere bakıyor", "TürkAnime GUI")
# Başlık
title_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
title_frame.pack(fill="x", pady=(20, 20))
back_btn = ctk.CTkButton(title_frame, text="← Ana Sayfa",
command=self.show_home,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"))
back_btn.pack(side="left")
trending_title = ctk.CTkLabel(title_frame, text="🔥 AniList Trend",
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
trending_title.pack(side="left", padx=30)
trend_desc = ctk.CTkLabel(title_frame, text="AniList topluluğunun en sevdiği animeler",
font=ctk.CTkFont(size=11),
text_color="#888888")
trend_desc.pack(side="left")
# Trend grid (use non-scrollable grid; page itself scrolls)
self.trending_full_grid = ctk.CTkFrame(self.content_area, fg_color="transparent")
self.trending_full_grid.pack(fill="both", expand=True)
# Loading
loading_label = ctk.CTkLabel(self.trending_full_grid, text="Trend animeler yükleniyor...",
font=ctk.CTkFont(size=16),
text_color="#888888")
loading_label.pack(pady=50)
# Trend animeleri yükle - Sadece AniList
def load_worker():
try:
trending = anilist_client.get_trending_anime(page=1, per_page=50)
self.after(0, lambda: self.display_full_trending(trending, loading_label))
except Exception as e:
error_msg = str(e)
self.after(0, lambda: self.show_error(error_msg, loading_label))
threading.Thread(target=load_worker, daemon=True).start()
def show_downloads(self):
"""İndirilenler sayfası göster."""
self.current_view = "downloads"
self.clear_content_area()
# Navigasyon butonlarını güncelle
self._update_nav_buttons("downloads")
# Discord Rich Presence güncelle
self.update_discord_presence("İndirilenlere bakıyor", "TürkAnime GUI")
# Başlık
title_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
title_frame.pack(fill="x", pady=(20, 20))
back_btn = ctk.CTkButton(title_frame, text="← Ana Sayfa",
command=self.show_home,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"))
back_btn.pack(side="left")
downloads_title = ctk.CTkLabel(title_frame, text="⬇️ İndirilenler",
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
downloads_title.pack(side="left", padx=30)
# Yenile butonu
refresh_btn = ctk.CTkButton(title_frame, text="🔄 Yenile",
command=self.show_downloads,
fg_color="#4ecdc4", hover_color="#45b7aa",
width=100, height=35)
refresh_btn.pack(side="right")
# İndirilenler grid
self.downloads_grid = ctk.CTkFrame(self.content_area, fg_color="transparent")
self.downloads_grid.pack(fill="both", expand=True)
# Loading
loading_label = ctk.CTkLabel(self.downloads_grid, text="İndirilenler yükleniyor...",
font=ctk.CTkFont(size=16),
text_color="#888888")
loading_label.pack(pady=50)
# İndirilen dosyaları yükle
def load_worker():
try:
downloads = self.get_downloaded_files()
self.after(0, lambda: self.display_downloads(downloads, loading_label))
except Exception as e:
error_msg = str(e)
self.after(0, lambda: self.show_error(error_msg, loading_label))
threading.Thread(target=load_worker, daemon=True).start()
def get_downloaded_files(self):
"""İndirilen dosyaları tara."""
import os
from pathlib import Path
downloads = []
try:
dosya = Dosyalar()
indirilenler_dir = dosya.ayarlar.get("indirilenler", ".")
# Önce mevcut indirilenler listesini ekle
downloads.extend(self.downloaded_episodes)
# Klasör taraması yap
if not os.path.exists(indirilenler_dir):
return downloads
# Tüm video dosyalarını tara
video_extensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm']
for root, dirs, files in os.walk(indirilenler_dir):
for file in files:
if any(file.lower().endswith(ext) for ext in video_extensions):
file_path = os.path.join(root, file)
file_size = os.path.getsize(file_path)
file_date = os.path.getmtime(file_path)
# Zaten listede var mı kontrol et
existing = False
for item in downloads:
if item.get('file_path') == file_path:
existing = True
break
if not existing:
# Anime adı ve bölüm numarasını çıkar
anime_name = os.path.basename(root)
episode_name = file
downloads.append({
'anime_name': anime_name,
'episode_name': episode_name,
'file_path': file_path,
'file_size': file_size,
'file_date': file_date
})
# Tarihe göre sırala (en yeni önce)
downloads.sort(key=lambda x: x['file_date'], reverse=True)
except Exception as e:
print(f"İndirilenler tarama hatası: {e}")
return downloads
def display_downloads(self, downloads, loading_label):
"""İndirilenleri göster."""
loading_label.destroy()
if not downloads:
no_downloads_label = ctk.CTkLabel(self.downloads_grid,
font=ctk.CTkFont(size=16),
text_color="#888888")
no_downloads_label.pack(pady=50)
return
# İndirilenler listesi
downloads_frame = ctk.CTkScrollableFrame(self.downloads_grid, fg_color="transparent")
downloads_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20))
for download in downloads:
# Dosya kartı
file_frame = ctk.CTkFrame(downloads_frame, fg_color="#2a2a2a", corner_radius=10)
file_frame.pack(fill="x", pady=5)
# Sol taraf - Dosya bilgileri
info_frame = ctk.CTkFrame(file_frame, fg_color="transparent")
info_frame.pack(side="left", fill="both", expand=True, padx=15, pady=10)
# Anime adı
anime_label = ctk.CTkLabel(info_frame, text=download['anime_name'],
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff")
anime_label.pack(anchor="w")
# Bölüm adı
episode_label = ctk.CTkLabel(info_frame, text=download['episode_name'],
font=ctk.CTkFont(size=12),
text_color="#cccccc")
episode_label.pack(anchor="w", pady=(2, 5))
# Dosya boyutu ve tarih
import time
size_mb = download['file_size'] / (1024 * 1024)
date_str = time.strftime('%d.%m.%Y %H:%M', time.localtime(download['file_date']))
details_label = ctk.CTkLabel(info_frame,
text=f"{size_mb:.1f} MB • {date_str}",
font=ctk.CTkFont(size=10),
text_color="#888888")
details_label.pack(anchor="w")
# Sağ taraf - Aksiyon butonları
actions_frame = ctk.CTkFrame(file_frame, fg_color="transparent")
actions_frame.pack(side="right", padx=15, pady=10)
# Oynat butonu
play_btn = ctk.CTkButton(actions_frame, text="▶️ Oynat",
command=lambda p=download['file_path']: self.play_local_file(p),
fg_color="#4ecdc4", hover_color="#45b7aa",
width=80, height=32)
play_btn.pack(side="left", padx=(0, 5))
# Klasörde göster butonu
show_btn = ctk.CTkButton(actions_frame, text="📁 Göster",
command=lambda p=download['file_path']: self.show_in_folder(p),
fg_color="#666666", width=80, height=32)
show_btn.pack(side="left")
def play_local_file(self, file_path):
"""Yerel dosyayı oynat."""
try:
import subprocess
import platform
if platform.system() == "Windows":
os.startfile(file_path)
elif platform.system() == "Darwin": # macOS
subprocess.run(["open", file_path])
else: # Linux
subprocess.run(["xdg-open", file_path])
self.message("Dosya açılıyor...", error=False)
except Exception as e:
self.message(f"Dosya açılırken hata: {e}", error=True)
def update_downloaded_list(self, bolum, down_dir):
"""İndirilenler listesini güncelle."""
try:
import os
from pathlib import Path
# İndirilen dosya yolunu oluştur
anime_slug = bolum.anime.slug if bolum.anime else ""
file_path = os.path.join(down_dir, anime_slug, bolum.slug + ".mp4")
# Dosya mevcut mu kontrol et
if os.path.exists(file_path):
# İndirilenler listesine ekle
# Zaten listede var mı kontrol et
existing = False
for item in self.downloaded_episodes:
if item.get('file_path') == file_path:
existing = True
break
if not existing:
self.downloaded_episodes.append({
'anime_name': bolum.anime.title if bolum.anime else "Bilinmeyen Anime",
'episode_name': bolum.title,
'file_path': file_path,
'file_size': os.path.getsize(file_path),
'file_date': os.path.getmtime(file_path)
})
# Listeyi tarihe göre sırala
self.downloaded_episodes.sort(key=lambda x: x['file_date'], reverse=True)
except Exception as e:
print(f"İndirilenler listesi güncelleme hatası: {e}")
def show_in_folder(self, file_path):
"""Dosyanın bulunduğu klasörü göster."""
try:
import subprocess
import platform
folder_path = os.path.dirname(file_path)
if platform.system() == "Windows":
subprocess.run(["explorer", "/select,", file_path])
elif platform.system() == "Darwin": # macOS
subprocess.run(["open", "-R", file_path])
else: # Linux
subprocess.run(["xdg-open", folder_path])
except Exception as e:
self.message(f"Klasör açılırken hata: {e}", error=True)
def display_full_trending(self, anime_list, loading_label):
"""Tam trend listesini göster - responsive grid."""
loading_label.destroy()
# Anime listesini sakla (resize için)
self._trending_anime_list = anime_list
# Responsive grid'i oluştur
self._update_trending_grid()
# Pencere resize event'i ekle
if not hasattr(self, '_trending_resize_bound'):
self._trending_resize_bound = False
if not self._trending_resize_bound:
self.bind("<Configure>", self._on_window_resize)
self._trending_resize_bound = True
def _calculate_columns(self, available_width):
"""Mevcut genişliğe göre sütun sayısı hesapla."""
card_width = 210 # Kart genişliği + padding
min_cols = 3
max_cols = 8
cols = max(min_cols, min(max_cols, available_width // card_width))
return cols
def _on_window_resize(self, event=None):
"""Pencere resize olduğunda grid'i güncelle."""
if not hasattr(self, 'current_view') or self.current_view != "trending":
return
if not hasattr(self, '_trending_anime_list') or not self._trending_anime_list:
return
# Debounce için timer kullan
if hasattr(self, '_resize_timer') and self._resize_timer is not None:
self.after_cancel(self._resize_timer)
self._resize_timer = self.after(150, self._update_trending_grid)
def _update_trending_grid(self):
"""Trending grid'i yeniden oluştur."""
if not hasattr(self, 'trending_full_grid') or not self.trending_full_grid.winfo_exists():
return
# Mevcut içeriği temizle
for widget in self.trending_full_grid.winfo_children():
widget.destroy()
# Sütun sayısını hesapla
try:
available_width = self.trending_full_grid.winfo_width()
if available_width < 100: # Widget henüz hazır değil
available_width = 1200
except:
available_width = 1200
max_cols = self._calculate_columns(available_width)
row = 0
col = 0
for anime in self._trending_anime_list:
if col >= max_cols:
col = 0
row += 1
self.create_anime_card(self.trending_full_grid, anime, row, col, max_cols)
col += 1
def show_watchlist(self):
"""İzleme listesi göster."""
if not anilist_client.access_token:
self.message("AniList girişi gerekli", error=True)
return
self.current_view = "watchlist"
self.clear_content_area()
# Navigasyon butonlarını güncelle
self._update_nav_buttons("watchlist")
# Başlık
title_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
title_frame.pack(fill="x", pady=(20, 20))
back_btn = ctk.CTkButton(title_frame, text="← Ana Sayfa",
command=self.show_home,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"))
back_btn.pack(side="left")
watchlist_title = ctk.CTkLabel(title_frame, text="📋 İzleme Listem",
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
watchlist_title.pack(side="left", padx=30)
# Liste tipi seçici
list_type_frame = ctk.CTkFrame(title_frame, fg_color="transparent")
list_type_frame.pack(side="right")
self.cmbListType = ctk.CTkComboBox(list_type_frame,
values=["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED"],
command=self.on_watchlist_type_change,
width=120, height=35)
self.cmbListType.pack()
self.cmbListType.set("CURRENT")
# Watchlist grid (non-scrollable; page scrolls)
self.watchlist_grid = ctk.CTkFrame(self.content_area, fg_color="transparent")
self.watchlist_grid.pack(fill="both", expand=True)
# Loading
loading_label = ctk.CTkLabel(self.watchlist_grid, text="İzleme listesi yükleniyor...",
font=ctk.CTkFont(size=16),
text_color="#888888")
loading_label.pack(pady=50)
# Watchlist yükle
self.load_watchlist(loading_label)
def load_watchlist(self, loading_label):
"""İzleme listesi yükle."""
def load_worker():
try:
if not self.anilist_user:
raise Exception("Kullanıcı bilgileri bulunamadı")
user_id = self.anilist_user.get('id')
if not user_id:
raise Exception("Kullanıcı ID bulunamadı")
list_type = self.cmbListType.get() if hasattr(self, 'cmbListType') else "CURRENT"
results = anilist_client.get_user_anime_list(user_id, list_type)
# Anime listesini çıkar
anime_list = []
for list_item in results:
for entry in list_item.get('entries', []):
anime_data = entry.get('media', {})
anime_data['user_progress'] = entry.get('progress', 0)
anime_data['user_score'] = entry.get('score', 0)
anime_data['user_status'] = entry.get('status', 'CURRENT')
anime_list.append(anime_data)
self.after(0, lambda: self.display_watchlist(anime_list, loading_label))
except Exception as e:
error_msg = str(e)
self.after(0, lambda: self.show_error(error_msg, loading_label))
threading.Thread(target=load_worker, daemon=True).start()
def display_watchlist(self, anime_list, loading_label):
"""İzleme listesini göster - responsive grid."""
loading_label.destroy()
if not anime_list:
empty_label = ctk.CTkLabel(self.watchlist_grid,
text="Bu kategoride anime bulunamadı",
font=ctk.CTkFont(size=16),
text_color="#888888")
empty_label.pack(pady=50)
return
# Anime listesini sakla (resize için)
self._watchlist_anime_list = anime_list
# Grid'i oluştur
self._update_watchlist_grid()
def _update_watchlist_grid(self):
"""Watchlist grid'i yeniden oluştur."""
if not hasattr(self, 'watchlist_grid') or not self.watchlist_grid.winfo_exists():
return
# Mevcut içeriği temizle
for widget in self.watchlist_grid.winfo_children():
widget.destroy()
# Sütun sayısını hesapla
try:
available_width = self.watchlist_grid.winfo_width()
if available_width < 100:
available_width = 1200
except:
available_width = 1200
max_cols = self._calculate_columns(available_width)
row = 0
col = 0
for anime in self._watchlist_anime_list:
if col >= max_cols:
col = 0
row += 1
self.create_watchlist_card(self.watchlist_grid, anime, row, col, max_cols)
col += 1
def create_watchlist_card(self, parent, anime_data, row, col, max_cols):
"""İzleme listesi kartı oluştur."""
card_frame = ctk.CTkFrame(parent, fg_color="#2a2a2a", width=180, height=320)
card_frame.grid(row=row, column=col, padx=4, pady=4, sticky="nw")
card_frame.pack_propagate(False)
# Hover efekti
def on_enter(e):
card_frame.configure(fg_color="#3a3a3a")
def on_leave(e):
card_frame.configure(fg_color="#2a2a2a")
card_frame.bind("<Enter>", on_enter)
card_frame.bind("<Leave>", on_leave)
# Kapak görseli
cover_frame = ctk.CTkFrame(card_frame, fg_color="#1a1a1a", width=160, height=200)
cover_frame.pack(pady=(10, 0))
cover_frame.pack_propagate(False)
cover_label = ctk.CTkLabel(cover_frame, text="", font=ctk.CTkFont(size=60))
cover_label.pack(expand=True)
# Kapak görselini yükle
cover_url = anime_data.get('coverImage', {}).get('large')
if cover_url:
self.load_anilist_thumbnail(cover_url, cover_label, 140, 180)
# Başlık
title_text = anime_data.get('title', {}).get('romaji', 'Unknown')
if len(title_text) > 18:
title_text = title_text[:15] + "..."
title_label = ctk.CTkLabel(card_frame, text=title_text,
font=ctk.CTkFont(size=11, weight="bold"),
text_color="#ffffff", wraplength=160)
title_label.pack(pady=(8, 0))
# Progress
user_progress = anime_data.get('user_progress', 0)
total_episodes = anime_data.get('episodes')
if user_progress is not None and total_episodes:
progress_text = f"İzlenen: {user_progress}/{total_episodes}"
progress_label = ctk.CTkLabel(card_frame, text=progress_text,
font=ctk.CTkFont(size=10),
text_color="#4ecdc4")
progress_label.pack(pady=(2, 0))
# Progress bar
progress_value = (user_progress / total_episodes) * 100
progress_bar = ctk.CTkProgressBar(card_frame, width=140, height=6)
progress_bar.pack(pady=(2, 0))
progress_bar.set(progress_value / 100)
# Skor
user_score = anime_data.get('user_score', 0)
if user_score and user_score > 0:
score_label = ctk.CTkLabel(card_frame, text=f"⭐ {user_score}/100",
font=ctk.CTkFont(size=10),
text_color="#ffd93d")
score_label.pack(pady=(2, 0))
# Status
user_status = anime_data.get('user_status', 'CURRENT')
status_colors = {
"CURRENT": "#4CAF50",
"PLANNING": "#2196F3",
"COMPLETED": "#9C27B0",
"DROPPED": "#F44336",
"PAUSED": "#FF9800"
}
status_color = status_colors.get(user_status, "#666666")
status_label = ctk.CTkLabel(card_frame, text=user_status,
font=ctk.CTkFont(size=9),
text_color=status_color)
status_label.pack(pady=(4, 8))
# Tıkla eventi
def on_click():
self.show_anime_details(anime_data)
card_frame.bind("<Button-1>", lambda e: on_click())
def _update_loading_status(self, message):
"""Loading durumunu güncelle."""
if hasattr(self, 'status_label'):
self.status_label.configure(text=message, text_color="#cccccc")
def message(self, text, error=False):
"""Durum mesajı göster."""
if hasattr(self, 'status_label'):
color = "#ff6b6b" if error else "#cccccc"
self.status_label.configure(text=text, text_color=color)
else:
print(f"Message: {text}")
def on_watchlist_type_change(self, list_type):
"""İzleme listesi tipi değişti."""
if hasattr(self, 'watchlist_grid'):
# Mevcut içeriği temizle
for widget in self.watchlist_grid.winfo_children():
widget.destroy()
# Loading
loading_label = ctk.CTkLabel(self.watchlist_grid, text="Yükleniyor...",
font=ctk.CTkFont(size=16),
text_color="#888888")
loading_label.pack(pady=50)
# Yeni listeyi yükle
self.load_watchlist(loading_label)
def show_trending_error(self, error_msg):
"""Trend yükleme hatası göster."""
if hasattr(self, 'loading_label'):
self.loading_label.configure(text=f"Hata: {error_msg}", text_color="#ff6b6b")
def show_error(self, error_msg, widget_to_replace):
"""Hata mesajı göster."""
widget_to_replace.configure(text=f"Hata: {error_msg}", text_color="#ff6b6b")
def search_anime_locally(self, title):
"""Anime'yi yerel kaynaklarda ara."""
# Yeni UI'da arama yapmak için searchEntry'yi kullan
if hasattr(self, 'searchEntry'):
self.searchEntry.delete(0, "end")
self.searchEntry.insert(0, title)
self.on_search()
else:
self.message("Arama özelliği mevcut değil", error=True)
def on_search(self):
"""Yeni UI için arama yap."""
if not hasattr(self, 'searchEntry'):
self.message("Arama özelliği mevcut değil", error=True)
return
query = self.searchEntry.get().strip()
if not query:
self.message("Arama terimi girin", error=True)
return
# Seçilen başlıktan başlık kısmını al
selected_title = ""
if hasattr(self, 'cmbTitle') and hasattr(self, 'selected_title'):
selected_title = getattr(self, 'selected_title', "")
if selected_title:
# "🇺🇸 İngilizce: " veya "🇯🇵 Romanji: " kısmını çıkar
if "İngilizce: " in selected_title:
selected_title = selected_title.split("İngilizce: ")[1]
elif "Romanji: " in selected_title:
selected_title = selected_title.split("Romanji: ")[1]
elif "Orijinal: " in selected_title:
selected_title = selected_title.split("Orijinal: ")[1]
else:
selected_title = ""
# Başlık seçimi varsa onu kullan, yoksa normal arama yap
search_query = selected_title if selected_title else query
# Discord Rich Presence güncelle
self.update_discord_presence(f"'{search_query}' arıyor", "TürkAnime GUI")
# Jikan (MAL) ile ara, başarısız olursa AniList'e fallback
self.message("Jikan (MAL) ile aranıyor…")
def search_worker():
try:
results = None
# Önce Jikan dene
if JIKAN_AVAILABLE and jikan_search_anime:
try:
results = jikan_search_anime(search_query, limit=20)
except Exception as jikan_err:
with open("debug.log", "a") as f:
f.write(f"WARN: Jikan arama başarısız, AniList'e fallback: {jikan_err}\n")
results = None
# Jikan başarısız olduysa AniList'e fallback
if not results:
self.after(0, lambda: self.message("Jikan başarısız, AniList deneniyor…"))
results = anilist_client.search_anime(search_query)
self.after(0, lambda: self.display_anilist_search_results(results, f"Arama: {search_query}"))
except Exception as e:
self.after(0, lambda: self.message(f"Arama hatası: {e}", error=True))
threading.Thread(target=search_worker, daemon=True).start()
# --- AniList Methods ---
def on_anilist_login(self):
"""AniList OAuth login."""
try:
# Start local auth server first (avoid race where browser redirects before server ready)
if not self.anilist_auth_server:
self.anilist_auth_server = AniListAuthServer(anilist_client)
# UI'yi güncellemek için success callback bağla
try:
# Başarılı girişte kullanıcı bilgisini UI'ye yansıt
def _on_success():
# UI güncelle
self.after(0, self.check_anilist_auth_status)
# Sunucu referansını temizle
self.anilist_auth_server = None
self.anilist_auth_server.register_on_success(_on_success)
except Exception:
pass
threading.Thread(target=self.anilist_auth_server.start_server, daemon=True).start()
# Open browser for OAuth (use authorization code flow: response_type=code)
auth_url = anilist_client.get_auth_url(response_type="code")
webbrowser.open(auth_url)
self.message("Tarayıcıda AniList girişini tamamlayın")
except Exception as e:
self.message(f"AniList giriş hatası: {e}", error=True)
self.lblAniListUser.configure(text="Giriş yapılmamış")
def on_anilist_logout(self):
"""AniList logout."""
try:
anilist_client.clear_tokens()
except Exception:
anilist_client.access_token = None
anilist_client.refresh_token = None
anilist_client.user_data = None
self.anilist_user = None
self.lblAniListUser.configure(text="Giriş yapılmamış")
self.message("AniList oturumu kapatıldı")
def on_anilist_search(self):
"""AniList'te ara."""
# Bu method artık kullanılmıyor, on_search kullanılıyor
pass
def on_anilist_trending(self):
"""Get trending anime from AniList."""
self.message("Trend animeler yükleniyor…")
def trending_worker():
try:
results = anilist_client.get_trending_anime()
self.after(0, lambda: self.display_anilist_results(results, "AniList Trendler"))
except Exception as e:
self.after(0, lambda: self.message(f"Trend yükleme hatası: {e}", error=True))
threading.Thread(target=trending_worker, daemon=True).start()
def on_anilist_watchlist(self):
"""Load user's AniList watchlist."""
if not anilist_client.access_token:
self.message("AniList girişi gerekli", error=True)
return
if not self.anilist_user:
self.message("Kullanıcı bilgileri yüklenemedi", error=True)
return
self.message("İzleme listesi yükleniyor…")
def watchlist_worker():
try:
if not self.anilist_user:
raise Exception("Kullanıcı bilgileri bulunamadı")
user_id = self.anilist_user.get('id')
if not user_id:
raise Exception("Kullanıcı ID bulunamadı")
list_type = self.anilist_current_list_type
results = anilist_client.get_user_anime_list(user_id, list_type)
# Extract anime from lists
anime_list = []
for list_item in results:
for entry in list_item.get('entries', []):
anime_data = entry.get('media', {})
anime_data['user_progress'] = entry.get('progress', 0)
anime_data['user_score'] = entry.get('score', 0)
anime_data['user_status'] = entry.get('status', 'CURRENT')
anime_list.append(anime_data)
self.after(0, lambda: self.display_anilist_results(anime_list, f"AniList {list_type} Listesi"))
except Exception as e:
self.after(0, lambda: self.message(f"İzleme listesi hatası: {e}", error=True))
threading.Thread(target=watchlist_worker, daemon=True).start()
def on_anilist_sync(self):
"""Sync progress with AniList."""
if not anilist_client.access_token:
self.message("AniList girişi gerekli", error=True)
return
self.message("AniList ile senkronize ediliyor…")
def sync_worker():
try:
self.sync_progress_with_anilist()
self.after(0, lambda: self.message("Senkronizasyon tamamlandı"))
except Exception as e:
self.after(0, lambda: self.message(f"Senkronizasyon hatası: {e}", error=True))
threading.Thread(target=sync_worker, daemon=True).start()
def on_anilist_list_type_change(self, list_type: str):
"""Handle list type change."""
self.anilist_current_list_type = list_type
if anilist_client.access_token and self.anilist_user:
self.on_anilist_watchlist()
def display_anilist_results(self, results: List[Dict], title: str):
"""AniList arama sonuçlarını göster."""
# İçerik alanını temizle
self.clear_content_area()
if not results:
no_results_label = ctk.CTkLabel(self.content_area, text="Sonuç bulunamadı",
font=ctk.CTkFont(size=16),
text_color="#888888")
no_results_label.pack(pady=50)
self.message("Sonuç bulunamadı")
return
# Başlık
title_frame = ctk.CTkFrame(self.content_area, fg_color="transparent")
title_frame.pack(fill="both", pady=(20, 20))
back_btn = ctk.CTkButton(title_frame, text="← Ana Sayfa",
command=self.show_home,
fg_color="transparent", text_color="#ff6b6b",
font=ctk.CTkFont(size=14, weight="bold"))
back_btn.pack(side="left")
search_title = ctk.CTkLabel(title_frame, text=title,
font=ctk.CTkFont(size=28, weight="bold"),
text_color="#ffffff")
search_title.pack(side="left", padx=30)
# Sonuçlar grid'i
results_grid = ctk.CTkFrame(self.content_area, fg_color="transparent", height=600)
results_grid.pack(fill="both", expand=True)
row = 0
col = 0
max_cols = 6
for anime in results[:30]: # Limit to 30 results
if col >= max_cols:
col = 0
row += 6
self.create_anime_card(results_grid, anime, row, col, max_cols)
col += 1
self.message(f"{len(results)} sonuç bulundu")
def display_anilist_search_results(self, results: List[Dict], title: str):
"""AniList arama sonuçlarını göster."""
self.display_anilist_results(results, title)
def add_anilist_to_library(self, anime_data: Dict):
"""AniList anime'yi yerel arama için kullan."""
try:
title = anime_data.get('title', {}).get('romaji', 'Unknown')
# Yeni UI'da arama yapmak için searchEntry'yi kullan
if hasattr(self, 'searchEntry'):
self.searchEntry.delete(0, "end")
self.searchEntry.insert(0, title)
self.on_search()
self.message(f"'{title}' için yerel arama başlatıldı")
else:
self.message("Arama özelliği mevcut değil", error=True)
except Exception as e:
self.message(f"Anime ekleme hatası: {e}", error=True)
def update_anilist_progress(self, anime_data: Dict):
"""AniList'e izleme ilerlemesini kaydet."""
try:
anime_id = anime_data.get('id')
if not anime_id:
raise Exception("Anime ID bulunamadı")
current_progress = anime_data.get('user_progress', 0)
total_episodes = anime_data.get('episodes', 0)
# Simple progress update dialog
from tkinter import simpledialog
progress_input = simpledialog.askstring(
f"Progress Güncelle - {anime_data.get('title', {}).get('romaji', 'Unknown')}",
f"İzlenen bölüm sayısı (0-{total_episodes}):",
initialvalue=str(current_progress)
)
if progress_input:
try:
new_progress = int(progress_input)
if 0 <= new_progress <= total_episodes:
success = anilist_client.update_anime_progress(anime_id, new_progress)
if success:
self.message(f"Progress güncellendi: {new_progress}/{total_episodes}")
# Refresh watchlist
self.on_anilist_watchlist()
else:
self.message("Progress güncelleme başarısız", error=True)
else:
self.message(f"Geçersiz progress: 0-{total_episodes} arası olmalı", error=True)
except ValueError:
self.message("Geçersiz sayı", error=True)
except Exception as e:
self.message(f"Progress güncelleme hatası: {e}", error=True)
def load_anilist_thumbnail(self, url: str, label: ctk.CTkLabel, width: int = 120, height: int = 160):
"""Load and display AniList thumbnail asynchronously with disk cache."""
if not url:
return
# Check memory cache first
cache_key = f"{url}_{width}_{height}"
if cache_key in self.anilist_image_cache:
try:
label.configure(image=self.anilist_image_cache[cache_key])
return
except:
pass
def load_image():
try:
# Disk cache için dosya adı oluştur
import hashlib
url_hash = hashlib.md5(cache_key.encode()).hexdigest()
cache_dir = os.path.join(appdirs.user_cache_dir("TurkAnime-GUI", "KebabLord"), "thumbnails")
os.makedirs(cache_dir, exist_ok=True)
cache_path = os.path.join(cache_dir, f"{url_hash}.png")
image = None
# Disk cache kontrolü
if os.path.exists(cache_path):
try:
image = Image.open(cache_path)
except:
pass
# Disk cache'de yoksa indir (retry mantığı ile)
if image is None:
max_retries = 3
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.2 Safari/605.1.15",
]
last_err = None
for attempt in range(max_retries):
try:
headers = {"User-Agent": user_agents[attempt % len(user_agents)]}
response = requests.get(url, timeout=20, headers=headers)
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
image.thumbnail((width, height), Image.Resampling.LANCZOS)
break
except Exception as retry_err:
last_err = retry_err
if attempt < max_retries - 1:
time.sleep(1.5 * (attempt + 1))
if image is None and last_err:
raise last_err # pylint: disable=raising-bad-type
if image is None:
raise RuntimeError("Thumbnail indirilemedi")
# Disk cache'e kaydet
try:
image.save(cache_path, "PNG")
except Exception:
pass
# Convert to CTkImage
ctk_image = ctk.CTkImage(image, size=(width, height))
# Memory cache'e kaydet
self.anilist_image_cache[cache_key] = ctk_image
# Update label in main thread - widget hala var mı kontrol et
def set_image():
try:
if label.winfo_exists():
label.configure(image=ctk_image)
except:
pass
self.after(0, set_image)
except Exception as e:
print(f"Thumbnail load error: {e}")
# Fallback to text - widget hala var mı kontrol et
def set_fallback():
try:
if label.winfo_exists():
label.configure(text="[Kapak]", image=None)
except:
pass
self.after(0, set_fallback)
threading.Thread(target=load_image, daemon=True).start()
def load_avatar(self, avatar_url, username):
"""Load and display user avatar."""
try:
# Avatar'ı indir
response = requests.get(avatar_url, timeout=10)
response.raise_for_status()
# PIL ile image'ı aç
image_data = Image.open(io.BytesIO(response.content))
# 32x32 boyutuna yeniden boyutlandır
image_data = image_data.resize((32, 32), Image.Resampling.LANCZOS)
# CTkImage oluştur
ctk_image = ctk.CTkImage(image_data, size=(32, 32))
# UI thread'de güncelle
self.after(0, lambda: self.update_avatar_display(ctk_image, username))
except Exception as e:
print(f"Avatar load error: {e}")
# Hata durumunda text göster
self.after(0, lambda: self.update_avatar_display(None, username))
def update_avatar_display(self, avatar_image, username):
"""Update avatar display in UI thread."""
if avatar_image:
self.avatarLabel.configure(image=avatar_image, text="")
self.lblAniListUser.configure(text=f"{username}")
else:
self.avatarLabel.configure(image="", text="")
self.lblAniListUser.configure(text=f"{username}")
def check_anilist_auth_status(self):
"""Check and update AniList authentication status."""
try:
if anilist_client.access_token:
user = anilist_client.get_current_user()
if user:
self.anilist_user = user
username = user.get('name', 'Unknown')
# Avatar URL'sini al
avatar_url = user.get('avatar', {}).get('large')
if avatar_url:
# Avatar'ı yükle ve göster
threading.Thread(target=self.load_avatar, args=(avatar_url, username), daemon=True).start()
else:
# Avatar yoksa text göster
self.lblAniListUser.configure(text=f"{username}")
self.avatarLabel.configure(image="", text="")
# Listem butonunu göster
if hasattr(self, 'btnWatchlist'):
self.btnWatchlist.pack(side="left", padx=1)
else:
self.lblAniListUser.configure(text="Giriş yapılmamış")
self.avatarLabel.configure(image="", text="")
# Listem butonunu gizle
if hasattr(self, 'btnWatchlist'):
self.btnWatchlist.pack_forget()
else:
self.lblAniListUser.configure(text="Giriş yapılmamış")
self.avatarLabel.configure(image="", text="")
# Listem butonunu gizle
if hasattr(self, 'btnWatchlist'):
self.btnWatchlist.pack_forget()
except Exception as e:
print(f"AniList auth check error: {e}")
self.lblAniListUser.configure(text="Giriş yapılmamış")
self.avatarLabel.configure(image="", text="")
# Hata durumunda da Listem butonunu gizle
if hasattr(self, 'btnWatchlist'):
self.btnWatchlist.pack_forget()
# --- Local Progress Tracking Methods ---
def load_local_progress(self):
"""Load local anime progress from storage."""
try:
import appdirs
data_dir = appdirs.user_data_dir("TurkAnime", "Barkeser2002")
os.makedirs(data_dir, exist_ok=True)
progress_file = os.path.join(data_dir, "anime_progress.json")
if os.path.exists(progress_file):
with open(progress_file, 'r', encoding='utf-8') as f:
self.local_anime_progress = json.load(f)
else:
self.local_anime_progress = {}
except Exception as e:
print(f"Local progress load error: {e}")
self.local_anime_progress = {}
def save_local_progress(self):
"""Save local anime progress to storage."""
try:
import appdirs
data_dir = appdirs.user_data_dir("TurkAnime", "Barkeser2002")
os.makedirs(data_dir, exist_ok=True)
progress_file = os.path.join(data_dir, "anime_progress.json")
with open(progress_file, 'w', encoding='utf-8') as f:
json.dump(self.local_anime_progress, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Local progress save error: {e}")
def update_local_progress(self, anime_title: str, progress: int, total_episodes: Optional[int] = None):
"""Update local progress for an anime."""
self.local_anime_progress[anime_title] = {
'progress': progress,
'total_episodes': total_episodes,
'last_updated': time.time()
}
self.save_local_progress()
def get_local_progress(self, anime_title: str) -> Dict:
"""Get local progress for an anime."""
return self.local_anime_progress.get(anime_title, {})
def on_play_selected(self):
"""Seçili bölümü veya ilk bölümü oynat."""
# Detay sayfasında seçili bölüm varsa onu oynat
if getattr(self, 'episodes_vars', None):
for var, obj in self.episodes_vars:
if var.get():
self._play_episode(obj)
return
# Seçili yoksa ilk bölümü oynat
if getattr(self, 'episodes_objs', None):
self._play_episode(self.episodes_objs[0])
return
self.message("Önce bir anime açıp bölüm seçin", error=True)
def on_download_selected(self):
"""Seçili bölümleri indir."""
if getattr(self, 'episodes_vars', None):
selected = [obj for var, obj in self.episodes_vars if var.get()]
if not selected and getattr(self, 'episodes_objs', None):
# Seçim yoksa ilk bölümü indir
selected = [self.episodes_objs[0]]
if selected:
def worker():
try:
dw = DownloadWorker(selected)
dw.signals.connect_progress(lambda msg: self.status_label.configure(text=msg))
dw.signals.connect_success(lambda: self.message("İndirme tamamlandı!"))
dw.signals.connect_error(lambda msg: self.message(f"İndirme hatası: {msg}", error=True))
dw.run()
except Exception as e:
self.message(f"İndirme başlatılamadı: {e}", error=True)
threading.Thread(target=worker, daemon=True).start()
return
self.message("Önce bir anime açıp bölüm seçin", error=True)
def load_episodes_and_play(self, anime: Anime):
"""Anime bölümlerini yükle ve oynat."""
def load_worker():
try:
self.status_label.configure(text=f"'{anime.title}' bölümleri yükleniyor...")
episodes = anime.bolumler
if episodes:
# İlk bölümü oynat
first_episode = episodes[0]
self.status_label.configure(text=f"'{first_episode.title}' oynatılıyor...")
# Video arama worker'ı başlat
video_worker = VideoFindWorker(first_episode)
video_worker.signals.connect_found(self.play_video)
video_worker.signals.connect_error(lambda msg: self.message(f"Hata: {msg}"))
threading.Thread(target=video_worker.run, daemon=True).start()
else:
self.message("Bölüm bulunamadı.")
except Exception as e:
self.message(f"Bölüm yükleme hatası: {str(e)}")
finally:
self.status_label.configure(text="TürkAnime hazır")
threading.Thread(target=load_worker, daemon=True).start()
def load_episodes_and_download(self, anime: Anime):
"""Anime bölümlerini yükle ve indir."""
def load_worker():
try:
self.status_label.configure(text=f"'{anime.title}' bölümleri yükleniyor...")
episodes = anime.bolumler
if episodes:
self.status_label.configure(text=f"{len(episodes)} bölüm indiriliyor...")
# Download worker başlat
download_worker = DownloadWorker(episodes)
download_worker.signals.connect_progress(lambda msg: self.status_label.configure(text=msg))
download_worker.signals.connect_success(lambda: self.message("İndirme tamamlandı!"))
download_worker.signals.connect_error(lambda msg: self.message(f"İndirme hatası: {msg}"))
threading.Thread(target=download_worker.run, daemon=True).start()
else:
self.message("Bölüm bulunamadı.")
except Exception as e:
self.message(f"Bölüm yükleme hatası: {str(e)}")
finally:
self.status_label.configure(text="TürkAnime hazır")
threading.Thread(target=load_worker, daemon=True).start()
def play_video(self, video_data):
"""Video'yu oynat."""
try:
# Bölüm objesini al ve Discord Rich Presence güncelle
episode_obj = video_data.get('bolum') if hasattr(video_data, 'get') else getattr(video_data, 'bolum', None)
anime_image = None
if episode_obj and episode_obj.anime:
anime_title = episode_obj.anime.title
episode_title = episode_obj.title
# Discord Rich Presence güncelle - AniList button'u ile
anime_data = self.selected_anime
anilist_id = anime_data.get('id') if anime_data else None
buttons = [
{
"label": "Uygulamayı Edin",
"url": "https://github.com/barkeser2002/turkanime-gui/releases"
}
]
if anilist_id:
buttons.append({
"label": "AniList'te Gör",
"url": f"https://anilist.co/anime/{anilist_id}"
})
self.update_discord_presence_anime(anime_title, f"Bölüm: {episode_title}", anime_image, buttons)
# Video player ile oynat (mpv, vlc vb.)
import subprocess
import platform
video_url = video_data.get('url')
if video_url:
if platform.system() == "Windows":
# Windows için mpv veya vlc kullan
try:
subprocess.run(["mpv", video_url], check=True)
except FileNotFoundError:
try:
subprocess.run(["vlc", video_url], check=True)
except FileNotFoundError:
self.message("Video oynatıcı bulunamadı. mpv veya vlc yükleyin.")
else:
# Linux/Mac için
try:
subprocess.run(["mpv", video_url], check=True)
except FileNotFoundError:
self.message("mpv bulunamadı. Lütfen yükleyin.")
self.message("Video oynatma tamamlandı.")
# İzleme ilerlemesi dialog'u aç
self.show_progress_dialog(video_data)
else:
self.message("Video URL bulunamadı.")
except Exception as e:
self.message(f"Video oynatma hatası: {str(e)}")
def show_progress_dialog(self, video_data):
"""İzleme ilerlemesi dialog'u göster."""
try:
# Bölüm objesini al
episode_obj = video_data.get('bolum') if hasattr(video_data, 'get') else getattr(video_data, 'bolum', None)
if not episode_obj:
return
# Anime bilgilerini al
anime_title = episode_obj.anime.title if episode_obj.anime else "Bilinmeyen Anime"
episode_title = episode_obj.title
# Dialog oluştur
dialog = ctk.CTkToplevel(self)
dialog.title("İzleme İlerlemesi")
dialog.geometry("400x250")
dialog.resizable(False, False)
# Dialog'u modal yap
dialog.transient(self)
dialog.grab_set()
# Başlık
title_label = ctk.CTkLabel(dialog, text="İzleme İlerlemesi Kaydet",
font=ctk.CTkFont(size=16, weight="bold"))
title_label.pack(pady=(20, 10))
# Anime bilgisi
info_frame = ctk.CTkFrame(dialog, fg_color="transparent")
info_frame.pack(fill="x", padx=20, pady=(0, 10))
anime_info = ctk.CTkLabel(info_frame, text=f"Anime: {anime_title}",
font=ctk.CTkFont(size=12))
anime_info.pack(anchor="w")
episode_info = ctk.CTkLabel(info_frame, text=f"Bölüm: {episode_title}",
font=ctk.CTkFont(size=12))
episode_info.pack(anchor="w")
# Bölüm sayısı girişi
input_frame = ctk.CTkFrame(dialog, fg_color="transparent")
input_frame.pack(fill="x", padx=20, pady=(10, 20))
input_label = ctk.CTkLabel(input_frame, text="Kaçıncı bölümü tamamladınız?",
font=ctk.CTkFont(size=12))
input_label.pack(anchor="w", pady=(0, 5))
episode_entry = ctk.CTkEntry(input_frame, placeholder_text="örn: 5")
episode_entry.pack(fill="x")
# Butonlar
button_frame = ctk.CTkFrame(dialog, fg_color="transparent")
button_frame.pack(fill="x", padx=20, pady=(0, 20))
def save_progress():
try:
episode_num = int(episode_entry.get().strip())
anime_id = self.selected_anime.get('id') if self.selected_anime else None
if not anime_id:
self.message("Anime ID bulunamadı", error=True)
return
# total_episodes her iki dalda da kullanılacağı için önce tanımla
total_episodes = self.selected_anime.get('episodes') if self.selected_anime else None
if episode_num > 0:
# normalize variable name used below
new_progress = episode_num
success = anilist_client.update_anime_progress(anime_id, new_progress)
if success:
self.message(f"Progress güncellendi: {new_progress}/{total_episodes}")
# Refresh watchlist
self.on_anilist_watchlist()
else:
self.message("Progress güncelleme başarısız", error=True)
else:
self.message(f"Geçersiz progress: 0-{total_episodes} arası olmalı", error=True)
except ValueError:
self.message("Geçersiz sayı", error=True)
def skip():
dialog.destroy()
save_btn = ctk.CTkButton(button_frame, text="Kaydet", command=save_progress,
fg_color="#4ecdc4", hover_color="#45b7aa", width=100)
save_btn.pack(side="left", padx=(0, 10))
skip_btn = ctk.CTkButton(button_frame, text="Atla", command=skip,
fg_color="#666666", width=100)
skip_btn.pack(side="left")
# Enter tuşu ile kaydet
def on_enter(event):
save_progress()
episode_entry.bind("<Return>", on_enter)
# Focus ayarla
episode_entry.focus()
except Exception as e:
print(f"Progress dialog hatası: {e}")
def save_anilist_progress(self, episode_obj, episode_num):
"""AniList'e izleme ilerlemesini kaydet."""
try:
if not anilist_client.access_token:
self.message("AniList girişi gerekli", error=True)
return
# Anime ID'sini bul
anime_title = episode_obj.anime.title if episode_obj.anime else ""
if not anime_title:
return
# AniList'te anime ara
search_results = anilist_client.search_anime(anime_title)
if not search_results:
self.message("Anime AniList'te bulunamadı", error=True)
return
# İlk sonucu al
anime_data = search_results[0]
anime_id = anime_data.get('id')
if anime_id:
# İlerlemesi güncelle
success = anilist_client.update_anime_progress(anime_id, episode_num)
if success:
self.message(f"AniList ilerlemesi güncellendi: Bölüm {episode_num}")
# Senkronizasyonu güncelle
self.sync_progress_with_anilist()
else:
self.message("AniList güncelleme başarısız", error=True)
except Exception as e:
self.message(f"AniList güncelleme hatası: {e}", error=True)
def sync_progress_with_anilist(self):
"""Sync local progress with AniList."""
if not anilist_client.access_token or not self.anilist_user:
return
try:
user_id = self.anilist_user.get('id')
if not user_id:
return
# Get user's current anime list
current_list = anilist_client.get_user_anime_list(user_id, "CURRENT")
# Update local progress with AniList data
for list_item in current_list:
for entry in list_item.get('entries', []):
anime = entry.get('media', {})
title = anime.get('title', {}).get('romaji', 'Unknown')
progress = entry.get('progress', 0)
total_episodes = anime.get('episodes')
self.update_local_progress(title, progress, total_episodes)
self.message("AniList ile senkronize edildi")
except Exception as e:
print(f"AniList sync error: {e}")
def on_source_change(self, source):
"""Kaynak değiştiğinde çağrılır."""
self.selected_source = source
self.message(f"Kaynak {source} olarak ayarlandı")
# Eğer anime detayları gösteriliyorsa bölümleri yeniden yükle
if hasattr(self, 'selected_anime') and self.selected_anime:
self.show_anime_details(self.selected_anime)
def load_more_episodes(self):
"""Daha fazla bölüm yükle."""
if self.is_loading_episodes or self.episodes_loaded >= len(self.all_episodes):
return
self.is_loading_episodes = True
# Bu sayfada yüklenecek bölüm sayısı
start_idx = self.episodes_loaded
end_idx = min(start_idx + self.episodes_per_page, len(self.all_episodes))
# Bölümleri yükle
for i in range(start_idx, end_idx):
episode = self.all_episodes[i]
row = ctk.CTkFrame(self.episodes_list, fg_color="transparent")
row.pack(fill="x", padx=10, pady=4)
var = ctk.BooleanVar(value=False)
chk = ctk.CTkCheckBox(row, text=f"{i+1:02d} - {episode['title']}", variable=var)
chk.pack(side="left")
# Sağ aksiyonlar
actions = ctk.CTkFrame(row, fg_color="transparent")
actions.pack(side="right")
btnPlay = ctk.CTkButton(actions, text="▶️", width=40, height=32,
command=lambda obj=episode['obj']: self._play_episode(obj))
btnPlay.pack(side="left", padx=(0, 5))
btnDl = ctk.CTkButton(actions, text="⬇️", width=40, height=32,
command=lambda obj=episode['obj']: self._download_episode(obj))
btnDl.pack(side="left")
self.episodes_vars.append((var, episode['obj']))
self.episodes_objs.append(episode['obj'])
self.episodes_loaded = end_idx
self.is_loading_episodes = False
# Eğer daha fazla bölüm varsa "Daha Fazla Yükle" butonu ekle
if self.episodes_loaded < len(self.all_episodes):
load_more_frame = ctk.CTkFrame(self.episodes_list, fg_color="transparent")
load_more_frame.pack(fill="x", padx=10, pady=10)
load_more_btn = ctk.CTkButton(load_more_frame, text="Daha Fazla Bölüm Yükle",
command=self.load_more_episodes)
load_more_btn.pack()
def toggle_anilist_panel(self):
"""AniList panelini göster/gizle."""
if self.anilist_visible:
# Gizle
self.anilist_panel.pack_forget()
self.btnAniListToggle.configure(text="👤 Göster")
self.anilist_visible = False
else:
# Göster
self.anilist_panel.pack(side="left", padx=(0, 2))
self.btnAniListToggle.configure(text="👤 Gizle")
self.anilist_visible = True
def show_user_tooltip(self, event):
"""Avatar'a hover yapıldığında kullanıcı adını göster."""
if hasattr(self, 'anilist_user') and self.anilist_user:
username = self.anilist_user.get('name', 'Unknown')
# Tooltip benzeri efekt için label rengini değiştir
self.lblAniListUser.configure(text_color="#ff6b6b", font=ctk.CTkFont(size=10, weight="bold"))
def hide_user_tooltip(self, event):
"""Kullanıcı tooltip'ini gizle."""
if hasattr(self, 'user_tooltip') and self.user_tooltip is not None:
self.user_tooltip.destroy()
self.user_tooltip = None
# Label rengini geri döndür
self.lblAniListUser.configure(text_color="#cccccc", font=ctk.CTkFont(size=9))
def open_anilist_page(self, anilist_id):
"""AniList sayfasını tarayıcıda aç."""
try:
import webbrowser
url = f"https://anilist.co/anime/{anilist_id}"
webbrowser.open(url)
self.message("AniList sayfası açılıyor...", error=False)
except Exception as e:
self.message(f"AniList sayfası açılamadı: {e}", error=True)
def run():
sep = ";" if os.name == "nt" else ":"
path_parts = [os.environ.get("PATH", "")]
# Kullanıcı app verisi
try:
path_parts.append(Dosyalar().ta_path)
except:
pass
# PyInstaller içindeysek _MEIPASS/bin
try:
_meipass = getattr(sys, "_MEIPASS", None)
if _meipass:
path_parts.append(os.path.join(_meipass, "bin"))
except Exception:
pass
# Geliştirme ortamında proje kökü altındaki bin
try:
root_bin = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "bin")
if os.path.isdir(root_bin):
path_parts.append(root_bin)
except Exception:
pass
os.environ["PATH"] = sep.join([p for p in path_parts if p])
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
app = MainWindow()
# Uygulama kapanırken Discord bağlantısını kapat
def on_closing():
try:
# Discord Rich Presence'i temizle
if hasattr(app, 'disconnect_discord_rpc'):
app.disconnect_discord_rpc()
elif hasattr(app, 'discord_rpc') and app.discord_rpc:
app.discord_rpc.clear()
app.discord_rpc.close()
except Exception as e:
print(f"Uygulama kapanırken Discord RPC kapatma hatası: {e}")
# Timer'ları iptal et
try:
if hasattr(app, 'discord_update_timer') and app.discord_update_timer:
app.after_cancel(app.discord_update_timer)
except Exception:
pass
app.destroy()
app.protocol("WM_DELETE_WINDOW", on_closing)
app.mainloop()
if __name__ == "__main__":
run()