add controller

This commit is contained in:
cheykrym 2026-04-01 03:35:38 +03:00
parent 8638c322a3
commit 04f5562e57
4 changed files with 305 additions and 24 deletions

15
controllers/__init__.py Normal file
View File

@ -0,0 +1,15 @@
from .media_controller import (
MediaController,
MediaSourceController,
BluetoothController,
CarPlayController,
TrackMetadata,
)
__all__ = [
"MediaController",
"MediaSourceController",
"BluetoothController",
"CarPlayController",
"TrackMetadata",
]

View File

@ -0,0 +1,247 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
from PySide6.QtCore import QObject, Signal
@dataclass
class TrackMetadata:
"""Метаданные трека."""
title: str = ""
artist: str = ""
album: str = ""
source: str = ""
position: int = 0 # ms
duration: int = 0 # ms
status: str = "" # "playing", "paused", "stopped"
class MediaSourceController(ABC):
"""Базовый класс контроллера источника медиа."""
@property
@abstractmethod
def name(self) -> str:
"""Название источника."""
pass
@abstractmethod
def get_volume(self) -> int:
"""Получить текущую громкость (0-100)."""
pass
@abstractmethod
def set_volume(self, value: int) -> None:
"""Установить громкость (0-100)."""
pass
@abstractmethod
def get_metadata(self) -> TrackMetadata:
"""Получить метаданные текущего трека."""
pass
@abstractmethod
def play(self) -> None:
"""Воспроизведение."""
pass
@abstractmethod
def pause(self) -> None:
"""Пауза."""
pass
@abstractmethod
def toggle_play(self) -> None:
"""Переключить воспроизведение/пауза."""
pass
@abstractmethod
def next_track(self) -> None:
"""Следующий трек."""
pass
@abstractmethod
def previous_track(self) -> None:
"""Предыдущий трек."""
pass
class BluetoothController(MediaSourceController):
"""Контроллер для Bluetooth аудио."""
def __init__(self, bt_service: Any):
self._bt_service = bt_service
@property
def name(self) -> str:
return "Bluetooth"
def get_volume(self) -> int:
# TODO: получить громкость из Bluetooth сервиса
return 50
def set_volume(self, value: int) -> None:
# TODO: установить громкость в Bluetooth сервис
pass
def get_metadata(self) -> TrackMetadata:
if self._bt_service is None:
return TrackMetadata()
metadata = self._bt_service.get_player_metadata()
return TrackMetadata(
title=metadata.get("title", ""),
artist=metadata.get("artist", ""),
album=metadata.get("album", ""),
source=metadata.get("source", ""),
position=metadata.get("position", 0) or 0,
duration=metadata.get("duration", 0) or 0,
status=metadata.get("status", ""),
)
def play(self) -> None:
if self._bt_service:
self._bt_service.player_play()
def pause(self) -> None:
if self._bt_service:
self._bt_service.player_pause()
def toggle_play(self) -> None:
if self._bt_service:
self._bt_service.player_toggle()
def next_track(self) -> None:
if self._bt_service:
self._bt_service.player_next()
def previous_track(self) -> None:
if self._bt_service:
self._bt_service.player_previous()
class CarPlayController(MediaSourceController):
"""Контроллер для CarPlay (заглушка)."""
def __init__(self):
self._volume = 50
@property
def name(self) -> str:
return "CarPlay"
def get_volume(self) -> int:
return self._volume
def set_volume(self, value: int) -> None:
self._volume = max(0, min(100, value))
def get_metadata(self) -> TrackMetadata:
# Заглушка - нет данных
return TrackMetadata(
title="",
artist="",
album="",
source="CarPlay",
position=0,
duration=0,
status="stopped",
)
def play(self) -> None:
# TODO: реализовать через CarPlay API
pass
def pause(self) -> None:
# TODO: реализовать через CarPlay API
pass
def toggle_play(self) -> None:
# TODO: реализовать через CarPlay API
pass
def next_track(self) -> None:
# TODO: реализовать через CarPlay API
pass
def previous_track(self) -> None:
# TODO: реализовать через CarPlay API
pass
class MediaController(QObject):
"""
Главный контроллер медиа.
Выбирает активный контроллер в зависимости от режима.
"""
volume_changed = Signal(int)
metadata_changed = Signal(object) # TrackMetadata
def __init__(self, bt_service: Any = None, parent: QObject = None):
super().__init__(parent)
self._bt_service = bt_service
self._controllers: dict[str, MediaSourceController] = {}
self._current_mode: str = "bluetooth"
# Регистрируем контроллеры
self._register_controllers()
def _register_controllers(self) -> None:
"""Зарегистрировать все доступные контроллеры."""
self._controllers["bluetooth"] = BluetoothController(self._bt_service)
self._controllers["carplay"] = CarPlayController()
# В будущем можно добавить:
# self._controllers["aux"] = AuxController()
def set_mode(self, mode: str) -> None:
"""Установить активный режим."""
if mode in self._controllers:
self._current_mode = mode
def get_mode(self) -> str:
"""Получить текущий режим."""
return self._current_mode
@property
def _current_controller(self) -> MediaSourceController:
"""Получить активный контроллер."""
return self._controllers.get(self._current_mode, self._controllers["bluetooth"])
# === Делегирующие методы ===
def get_volume(self) -> int:
"""Получить текущую громкость."""
return self._current_controller.get_volume()
def set_volume(self, value: int) -> None:
"""Установить громкость."""
self._current_controller.set_volume(value)
self.volume_changed.emit(value)
def get_metadata(self) -> TrackMetadata:
"""Получить метаданные текущего трека."""
return self._current_controller.get_metadata()
def play(self) -> None:
"""Воспроизведение."""
self._current_controller.play()
def pause(self) -> None:
"""Пауза."""
self._current_controller.pause()
def toggle_play(self) -> None:
"""Переключить воспроизведение/пауза."""
self._current_controller.toggle_play()
def next_track(self) -> None:
"""Следующий трек."""
self._current_controller.next_track()
def previous_track(self) -> None:
"""Предыдущий трек."""
self._current_controller.previous_track()

View File

@ -10,15 +10,15 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt, QSize, QTimer, Signal
from PySide6.QtGui import QFont
from services.bluetooth_service import BluetoothService
from controllers.media_controller import MediaController, TrackMetadata
class MediaScreen(QWidget):
source_changed = Signal(str)
def __init__(self):
def __init__(self, media_controller: MediaController = None):
super().__init__()
self._bt_service = BluetoothService(self)
self._controller = media_controller
root = QVBoxLayout(self)
root.setContentsMargins(18, 16, 18, 16)
@ -155,43 +155,54 @@ class MediaScreen(QWidget):
self._poll_timer.start(2000)
self._refresh_metadata()
def set_controller(self, controller: MediaController) -> None:
"""Установить контроллер."""
self._controller = controller
self._refresh_metadata()
def _toggle_play(self):
"""Переключить воспроизведение/пауза."""
self._bt_service.player_toggle()
if self._controller:
self._controller.toggle_play()
QTimer.singleShot(300, self._refresh_metadata)
def _prev(self):
"""Предыдущий трек."""
self._bt_service.player_previous()
if self._controller:
self._controller.previous_track()
QTimer.singleShot(300, self._refresh_metadata)
def _next(self):
"""Следующий трек."""
self._bt_service.player_next()
if self._controller:
self._controller.next_track()
QTimer.singleShot(300, self._refresh_metadata)
def _refresh_metadata(self):
"""Обновить метаданные трека."""
metadata = self._bt_service.get_player_metadata()
if not self._controller:
return
metadata = self._controller.get_metadata()
if metadata["title"]:
self.title.setText(metadata["title"])
if metadata["artist"]:
self.artist.setText(metadata["artist"])
if metadata["album"]:
self.album.setText(metadata["album"])
if metadata["source"]:
text = f"Источник: {metadata["source"]}"
if metadata.title:
self.title.setText(metadata.title)
if metadata.artist:
self.artist.setText(metadata.artist)
if metadata.album:
self.album.setText(metadata.album)
if metadata.source:
text = f"Источник: {metadata.source}"
self.source.setText(text)
self.source_changed.emit(text)
if metadata["duration"] is not None and metadata["duration"] > 0:
self.progress.setRange(0, metadata["duration"])
self.time_total.setText(self._format_time(metadata["duration"]))
if metadata["position"] is not None:
self.progress.setValue(metadata["position"])
self.time_pos.setText(self._format_time(metadata["position"]))
if metadata["status"]:
self.btn_play.setText("" if metadata["status"] == "playing" else "")
if metadata.duration is not None and metadata.duration > 0:
self.progress.setRange(0, metadata.duration)
self.time_total.setText(self._format_time(metadata.duration))
if metadata.position is not None:
self.progress.setValue(metadata.position)
self.time_pos.setText(self._format_time(metadata.position))
if metadata.status:
self.btn_play.setText("" if metadata.status == "playing" else "")
def _format_time(self, ms: int) -> str:
"""Форматировать время из миллисекунд."""

View File

@ -16,6 +16,8 @@ from themes import THEME_DAY, THEME_NIGHT
from screens.media import MediaScreen
from screens.stub import StubScreen
from screens.settings import SettingsScreen
from services.bluetooth_service import BluetoothService
from controllers.media_controller import MediaController
class MainWindowNew(QMainWindow):
@ -98,7 +100,11 @@ class MainWindowNew(QMainWindow):
top.addWidget(self.btn_settings)
self.stack = QStackedWidget()
self.media_screen = MediaScreen()
# Создаём сервис и контроллер
self._bt_service = BluetoothService(self)
self._media_controller = MediaController(self._bt_service, self)
self.media_screen = MediaScreen(self._media_controller)
self.stack.addWidget(self.media_screen) # 0
self.stack.addWidget(StubScreen("Car")) # 1
self.stack.addWidget(StubScreen("Maps")) # 2
@ -112,6 +118,7 @@ class MainWindowNew(QMainWindow):
# Читаем режим источника
self._source_mode = self._settings.value("media/source_mode", "bluetooth")
self._media_controller.set_mode(self._source_mode)
self._update_source_menu()
self.act_media.triggered.connect(lambda: self._open_media())
@ -191,6 +198,7 @@ class MainWindowNew(QMainWindow):
"""Установить режим источника (Bluetooth/CarPlay)."""
self._settings.setValue("media/source_mode", mode)
self._source_mode = mode
self._media_controller.set_mode(mode)
self._update_source_menu()
# Обновляем заголовок
if self.stack.currentIndex() == 0: