Загрузка данных
import sys
import os
import json
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QScrollArea,
QGridLayout, QStackedWidget, QSlider, QFrame)
from PyQt6.QtCore import Qt, QUrl
from PyQt6.QtGui import QPixmap, QFont, QCursor
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtMultimediaWidgets import QVideoWidget
DATA_FILE = "data.json"
def setup_filesystem():
"""Создает папки и начальный data.json, если их нет."""
for folder in ["posters", "series", "films"]:
os.makedirs(folder, exist_ok=True)
if not os.path.exists(DATA_FILE):
data = [
{
"id": "rozi_i_grehi",
"title": "Розы и грехи",
"category": "Турецкий сериал",
"description": "Успешный бизнесмен Серхат строил бизнес и свою жизнь на принципах порядочности и доверия. Его идеальный мир рушится, когда он узнаёт шокирующий секрет своей жены Беррак. Вскоре она впадает в кому после несчастного случая. Отправившись на поиски правды, Серхат встречает добрую и смелую девушку-цветочницу из другого мира — Зейнеп. Разница в социальном статусе не становится преградой для их чувств, но вместе им предстоит вырваться из сетей чужой лжи, тайн и предательств.",
"type": "series",
"poster": "rozi.png",
"episodes": [
{
"title": "25 серия",
"voiceover": "AlisaDirilish",
"duration": "02:15:00",
"file": "rozi25bolum.mp4"
}
]
}
]
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
class ItemCard(QFrame):
"""Карточка фильма/сериала для каталога."""
def __init__(self, item_data, callback):
super().__init__()
self.item_data = item_data
self.callback = callback
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setObjectName("card")
layout = QVBoxLayout(self)
self.poster = QLabel()
poster_path = os.path.join("posters", item_data.get("poster", ""))
if os.path.exists(poster_path):
pixmap = QPixmap(poster_path).scaled(200, 300, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
self.poster.setPixmap(pixmap)
else:
self.poster.setText("Нет постера")
self.poster.setFixedSize(200, 300)
self.poster.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.poster.setStyleSheet("background-color: #333; color: white; border-radius: 5px;")
self.poster.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title = QLabel(item_data["title"])
self.title.setFont(QFont("Arial", 12, QFont.Weight.Bold))
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title.setWordWrap(True)
layout.addWidget(self.poster)
layout.addWidget(self.title)
def mousePressEvent(self, event):
self.callback(self.item_data)
class BiCinemaApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("BiCinema")
self.resize(1000, 700)
self.current_item = None
self.apply_global_styles()
self.init_ui()
self.load_data()
def apply_global_styles(self):
self.setStyleSheet("""
QMainWindow, QWidget#main_bg {
background-color: #0F0F0F;
color: #FFFFFF;
}
QLabel {
color: #FFFFFF;
}
QFrame#card {
background-color: #1A1A1A;
border-radius: 10px;
}
QFrame#card:hover {
background-color: #2A2A2A;
}
QPushButton#red_btn {
background-color: #E50914;
color: white;
border-radius: 5px;
padding: 12px 20px;
font-size: 16px;
font-weight: bold;
border: none;
}
QPushButton#red_btn:hover {
background-color: #F40612;
}
QPushButton#back_btn {
background-color: transparent;
color: #AAAAAA;
font-size: 16px;
text-align: left;
border: none;
padding: 5px;
}
QPushButton#back_btn:hover {
color: #FFFFFF;
}
QScrollArea {
border: none;
background-color: transparent;
}
QScrollBar:vertical {
background: #111;
width: 10px;
}
QScrollBar::handle:vertical {
background: #444;
border-radius: 5px;
}
""")
def init_ui(self):
# Главный виджет, который меняет экраны
self.stacked_widget = QStackedWidget()
self.setCentralWidget(self.stacked_widget)
self.setup_catalog_screen()
self.setup_detail_screen()
self.setup_series_screen()
self.setup_player_screen()
def setup_catalog_screen(self):
catalog_widget = QWidget()
catalog_widget.setObjectName("main_bg")
layout = QVBoxLayout(catalog_widget)
# Логотип
logo = QLabel("BiCinema")
logo.setFont(QFont("Arial", 32, QFont.Weight.Bold))
logo.setAlignment(Qt.AlignmentFlag.AlignCenter)
logo.setStyleSheet("margin-top: 20px; margin-bottom: 20px;")
layout.addWidget(logo)
# Область с фильмами
scroll = QScrollArea()
scroll.setWidgetResizable(True)
self.grid_container = QWidget()
self.grid_container.setObjectName("main_bg")
self.grid_layout = QGridLayout(self.grid_container)
self.grid_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
scroll.setWidget(self.grid_container)
layout.addWidget(scroll)
self.stacked_widget.addWidget(catalog_widget) # Index 0
def setup_detail_screen(self):
detail_widget = QWidget()
detail_widget.setObjectName("main_bg")
layout = QVBoxLayout(detail_widget)
back_btn = QPushButton("← В каталог")
back_btn.setObjectName("back_btn")
back_btn.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(0))
layout.addWidget(back_btn, alignment=Qt.AlignmentFlag.AlignLeft)
# Заголовок и категория сверху
self.detail_title = QLabel()
self.detail_title.setFont(QFont("Arial", 28, QFont.Weight.Bold))
self.detail_category = QLabel()
self.detail_category.setFont(QFont("Arial", 14))
self.detail_category.setStyleSheet("color: #AAAAAA; margin-bottom: 20px;")
layout.addWidget(self.detail_title)
layout.addWidget(self.detail_category)
# Контент: Описание слева, Постер + Кнопка справа
content_layout = QHBoxLayout()
self.detail_desc = QLabel()
self.detail_desc.setFont(QFont("Arial", 12))
self.detail_desc.setWordWrap(True)
self.detail_desc.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
content_layout.addWidget(self.detail_desc, stretch=2)
right_vbox = QVBoxLayout()
self.detail_poster = QLabel()
self.detail_poster.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.detail_action_btn = QPushButton()
self.detail_action_btn.setObjectName("red_btn")
right_vbox.addWidget(self.detail_poster)
right_vbox.addWidget(self.detail_action_btn)
right_vbox.addStretch()
content_layout.addLayout(right_vbox, stretch=1)
layout.addLayout(content_layout)
self.stacked_widget.addWidget(detail_widget) # Index 1
def setup_series_screen(self):
series_widget = QWidget()
series_widget.setObjectName("main_bg")
layout = QVBoxLayout(series_widget)
back_btn = QPushButton("← Назад к описанию")
back_btn.setObjectName("back_btn")
back_btn.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(1))
layout.addWidget(back_btn, alignment=Qt.AlignmentFlag.AlignLeft)
title = QLabel("Выбор серии")
title.setFont(QFont("Arial", 24, QFont.Weight.Bold))
layout.addWidget(title)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
self.series_container = QWidget()
self.series_container.setObjectName("main_bg")
self.series_vbox = QVBoxLayout(self.series_container)
self.series_vbox.setAlignment(Qt.AlignmentFlag.AlignTop)
scroll.setWidget(self.series_container)
layout.addWidget(scroll)
self.stacked_widget.addWidget(series_widget) # Index 2
def setup_player_screen(self):
player_widget = QWidget()
player_widget.setStyleSheet("background-color: black;")
layout = QVBoxLayout(player_widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Верхняя панель (кнопка назад)
top_bar = QWidget()
top_bar_layout = QHBoxLayout(top_bar)
top_bar_layout.setContentsMargins(10, 10, 10, 10)
self.player_back_btn = QPushButton("← Назад")
# Белые акценты для плеера (как было запрошено)
white_btn_style = """
QPushButton {
background-color: transparent;
color: white;
border: 2px solid white;
border-radius: 5px;
padding: 8px 15px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 0.2);
}
"""
self.player_back_btn.setStyleSheet(white_btn_style)
self.player_back_btn.clicked.connect(self.stop_and_exit_player)
top_bar_layout.addWidget(self.player_back_btn, alignment=Qt.AlignmentFlag.AlignLeft)
# Видео виджет
self.player = QMediaPlayer()
self.audio_output = QAudioOutput()
self.player.setAudioOutput(self.audio_output)
self.video_widget = QVideoWidget()
self.player.setVideoOutput(self.video_widget)
# Нижняя панель (управление)
bottom_bar = QWidget()
bottom_bar_layout = QHBoxLayout(bottom_bar)
bottom_bar_layout.setContentsMargins(15, 10, 15, 15)
self.play_pause_btn = QPushButton("Пауза")
self.play_pause_btn.setStyleSheet(white_btn_style)
self.play_pause_btn.clicked.connect(self.toggle_play)
self.slider = QSlider(Qt.Orientation.Horizontal)
self.slider.setStyleSheet("""
QSlider::groove:horizontal { background: #333; height: 8px; border-radius: 4px; }
QSlider::sub-page:horizontal { background: white; height: 8px; border-radius: 4px; }
QSlider::handle:horizontal { background: white; width: 16px; margin-top: -4px; margin-bottom: -4px; border-radius: 8px; }
""")
self.slider.sliderMoved.connect(self.set_position)
self.speed_btn = QPushButton("1.0x")
self.speed_btn.setStyleSheet(white_btn_style)
self.speed_btn.clicked.connect(self.toggle_speed)
bottom_bar_layout.addWidget(self.play_pause_btn)
bottom_bar_layout.addWidget(self.slider)
bottom_bar_layout.addWidget(self.speed_btn)
layout.addWidget(top_bar)
layout.addWidget(self.video_widget, stretch=1)
layout.addWidget(bottom_bar)
self.player.positionChanged.connect(self.update_slider)
self.player.durationChanged.connect(self.update_duration)
self.stacked_widget.addWidget(player_widget) # Index 3
def load_data(self):
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
print("Ошибка загрузки данных:", e)
data = []
row, col = 0, 0
for item in data:
card = ItemCard(item, self.open_detail)
self.grid_layout.addWidget(card, row, col)
col += 1
if col > 3:
col = 0
row += 1
def open_detail(self, item):
self.current_item = item
self.detail_title.setText(item["title"])
self.detail_category.setText(item["category"])
self.detail_desc.setText(item["description"])
poster_path = os.path.join("posters", item.get("poster", ""))
if os.path.exists(poster_path):
pixmap = QPixmap(poster_path).scaled(300, 450, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
self.detail_poster.setPixmap(pixmap)
else:
self.detail_poster.clear()
self.detail_poster.setText("Нет постера")
# Отвязываем старые сигналы от кнопки, чтобы не накапливались
try: self.detail_action_btn.clicked.disconnect()
except: pass
if item["type"] == "film":
self.detail_action_btn.setText("Смотреть")
self.detail_action_btn.clicked.connect(lambda: self.play_video("films", item["file"]))
else:
self.detail_action_btn.setText("Выбрать серию")
self.detail_action_btn.clicked.connect(self.open_series_list)
self.stacked_widget.setCurrentIndex(1)
def open_series_list(self):
# Очищаем старый список
for i in reversed(range(self.series_vbox.count())):
widget = self.series_vbox.itemAt(i).widget()
if widget:
widget.setParent(None)
for ep in self.current_item.get("episodes", []):
ep_widget = QFrame()
ep_widget.setStyleSheet("background-color: #1e1e1e; border-radius: 8px; padding: 15px;")
ep_layout = QHBoxLayout(ep_widget)
info_vbox = QVBoxLayout()
title_lbl = QLabel(ep["title"])
title_lbl.setFont(QFont("Arial", 16, QFont.Weight.Bold))
voice_lbl = QLabel(f"Озвучка: {ep.get('voiceover', 'Неизвестно')} | Длительность: {ep.get('duration', '--:--')}")
voice_lbl.setStyleSheet("color: #aaa;")
info_vbox.addWidget(title_lbl)
info_vbox.addWidget(voice_lbl)
play_btn = QPushButton("Смотреть")
play_btn.setObjectName("red_btn")
play_btn.clicked.connect(lambda checked=False, f=ep["file"]: self.play_video("series", f))
ep_layout.addLayout(info_vbox)
ep_layout.addStretch()
ep_layout.addWidget(play_btn)
self.series_vbox.addWidget(ep_widget)
self.series_vbox.addStretch()
self.stacked_widget.setCurrentIndex(2)
def play_video(self, folder, filename):
path = os.path.join(os.getcwd(), folder, filename)
self.player.setSource(QUrl.fromLocalFile(path))
self.player.play()
self.play_pause_btn.setText("Пауза")
self.player.setPlaybackRate(1.0)
self.speed_btn.setText("1.0x")
self.stacked_widget.setCurrentIndex(3)
def stop_and_exit_player(self):
self.player.stop()
if self.current_item["type"] == "film":
self.stacked_widget.setCurrentIndex(1)
else:
self.stacked_widget.setCurrentIndex(2)
def toggle_play(self):
if self.player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
self.player.pause()
self.play_pause_btn.setText("Играть")
else:
self.player.play()
self.play_pause_btn.setText("Пауза")
def toggle_speed(self):
current = self.player.playbackRate()
if current == 1.0:
self.player.setPlaybackRate(1.25)
self.speed_btn.setText("1.25x")
elif current == 1.25:
self.player.setPlaybackRate(1.5)
self.speed_btn.setText("1.5x")
elif current == 1.5:
self.player.setPlaybackRate(2.0)
self.speed_btn.setText("2.0x")
else:
self.player.setPlaybackRate(1.0)
self.speed_btn.setText("1.0x")
def update_slider(self, position):
self.slider.setValue(position)
def update_duration(self, duration):
self.slider.setRange(0, duration)
def set_position(self, position):
self.player.setPosition(position)
if __name__ == "__main__":
setup_filesystem() # Автоматически создает папки и json
app = QApplication(sys.argv)
window = BiCinemaApp()
window.show()
sys.exit(app.exec())