"""Base synchronizer abstract class. Mirrors Sony.Filtr.PlaylistSynchronization.Synchronizer.SynchronizerBase from .NET. All platform synchronizers inherit from this class. """ import logging from abc import ABC, abstractmethod from collections.abc import Iterable from sqlalchemy.ext.asyncio import AsyncSession from playlist_sync.models.generic_playlist import GenericPlaylist, GenericTrack from playlist_sync.models.playlist_sync_result import PlaylistSynchronizationResult from playlist_sync.models.service_account import ServiceAccount from playlist_sync.models.sync_task import PlaylistSynchronization logger = logging.getLogger("playlist_sync.synchronizer") def warn_if_source_has_duplicates( tracks: Iterable[GenericTrack], target_name: str, sync_id: int | None, ) -> None: """Log a warning when the source playlist contains duplicate spotify_uris. Used by platform synchronizers whose target API does not preserve duplicate tracks (Deezer, YouTube, SoundCloud) — the sync still succeeds, but the target will hold only unique tracks. Spotify-target syncs do preserve duplicates and therefore do not call this helper. """ seen: set[str] = set() for track in tracks: if track.spotify_uri in seen: logger.warning( "Source playlist contains duplicate tracks; %s target will hold " "unique tracks only (sync_id=%s).", target_name, sync_id, ) return seen.add(track.spotify_uri) class BaseSynchronizer(ABC): """Abstract base for all platform synchronizers. Wraps _copy_internal in a try/except so any unhandled exception produces a well-formed PlaylistSynchronizationResult rather than propagating the exception to the worker task. Mirrors: SynchronizerBase.CopyToPlaylistAsync() """ async def copy_to_playlist( self, generic_playlist: GenericPlaylist, sync: PlaylistSynchronization, service_account: ServiceAccount, session: AsyncSession, ) -> PlaylistSynchronizationResult: try: return await self._copy_internal( generic_playlist, sync, service_account, session ) except Exception as exc: logger.exception("Unhandled error in synchronizer: %s", exc) return PlaylistSynchronizationResult.from_exception(exc) @abstractmethod async def _copy_internal( self, generic_playlist: GenericPlaylist, sync: PlaylistSynchronization, service_account: ServiceAccount, session: AsyncSession, ) -> PlaylistSynchronizationResult: """Platform-specific sync logic. Must be implemented by each subclass.""" ...