class Login(metaclass=Observable): on_start = signal() on_success = signal(str) on_failure = signal(Exception) on_connection_failed = signal() on_authentication_failed = signal() def __init__(self, session): self._session = session def authentication_started(self): self.on_start.emit() def authentication_succeeded(self, user_details): self._session.login_as(user_details["email"], user_details["token"], user_details["permissions"]) self.on_success.emit(user_details["email"]) def authentication_failed(self, error): if isinstance(error, PlatformConnectionError): self.on_connection_failed.emit() if isinstance(error, AuthenticationError): self.on_authentication_failed.emit() self.on_failure.emit(error)
class MediaPlayer(metaclass=Observable): class Error(Enum): none = QMediaPlayer.NoError unsupported_format = QMediaPlayer.FormatError access_denied = QMediaPlayer.AccessDeniedError closed = signal() playing = signal(Track) stopped = signal(Track) error_occurred = signal(Track) def __init__(self): self._media_library = create_media_library() self._playlist = [] self._player = QMediaPlayer() self._player.stateChanged.connect(self._state_changed) self._player.mediaStatusChanged.connect(self._media_status_changed) def play(self, track): self._playlist.append(track) self._player.setMedia(self._media_library.fetch(track.filename)) self._player.play() def stop(self): self._player.stop() def _media_status_changed(self, state): if state == QMediaPlayer.BufferedMedia: self._error = QMediaPlayer.NoError self.playing.emit(self._playlist[0]) elif state == QMediaPlayer.InvalidMedia: # On windows 8, we only get an InvalidMedia status in case of error, # so we need to keep track of the error that occurred self._error = QMediaPlayer.FormatError @property def error(self): return MediaPlayer.Error(self._player.error() or self._error) def _state_changed(self, state): if state == QMediaPlayer.StoppedState and self.error is not MediaPlayer.Error.none: self.error_occurred.emit(self._playlist.pop(0), self.error) elif state == QMediaPlayer.StoppedState: self.stopped.emit(self._playlist.pop(0)) def dispose(self): self._player.stop() # force the deletion of the player on windows because windows # doesn't release the handle and prevents further instanciations sip.delete(self._player) self._player = None self._media_library.dispose() self.closed.emit()
class IdentitySelection(metaclass=Observable): on_identities_available = signal(Identities) on_failure = signal(Exception) on_connection_failed = signal() on_permission_denied = signal() on_success = signal() on_lookup_start = signal() on_assignation_start = signal() on_insufficient_information = signal() def __init__(self, project, person): self._person = self._query = person self._project = project @property def person(self): return self._person @property def query(self): return self._query @property def works(self): return [track.track_title for track in self._project.tracks] def query_changed(self, query): self._query = query def lookup_started(self): self.on_lookup_start.emit() def assignation_started(self): self.on_assignation_start.emit() def identities_found(self, identities): identity_cards = [ IdentityCard(**identity) for identity in identities["identities"] ] self.on_identities_available.emit( Identities(identities["total_count"], identity_cards)) def failed(self, error): if isinstance(error, PlatformConnectionError): self.on_connection_failed.emit() if isinstance(error, PermissionDeniedError): self.on_permission_denied.emit() if isinstance(error, InsufficientInformationError): self.on_insufficient_information.emit() self.on_failure.emit(error) def identity_selected(self, identity): self._project.add_isni(self._person, identity.id) self.on_success.emit() def identity_assigned(self, identity): self.identity_selected(IdentityCard(**identity))
class ProjectHistory(metaclass=Observable): on_history_changed = signal() def __init__(self, *past_projects): self._history = deque(past_projects, maxlen=10) def project_opened(self, project): self._add_to_history(project) def project_saved(self, project): self._add_to_history(project) def _add_to_history(self, project): stale_entry = self._snapshot_for(project) if stale_entry is not None: self._history.remove(stale_entry) self._history.appendleft(ProjectSnapshot.of(project)) self.on_history_changed.emit() def _snapshot_for(self, project): return next( filter(lambda entry: entry.path == project.filename, self._history), None) def __getitem__(self, index): return self._history[index] def __len__(self): return len(self._history)
def __new__(mcs, clsname, bases, methods): methods["metadata_changed"] = signal(Signal.SELF) # Attach attribute names to the tags for key, value in methods.items(): if isinstance(value, Tag): value.name = key return super().__new__(mcs, clsname, bases, methods)
class Track(metaclass=tag.Taggable): chain_of_title_changed = signal(ChainOfTitle) album = None track_title = tag.text() lead_performer = tag.text() version_info = tag.text() featured_guest = tag.text() comments = tag.text() publisher = tag.sequence() lyricist = tag.sequence() composer = tag.sequence() isrc = tag.text() iswc = tag.text() labels = tag.text() lyrics = tag.text() language = tag.text() tagger = tag.text() tagger_version = tag.text() tagging_time = tag.text() track_number = tag.numeric() total_tracks = tag.numeric() recording_time = tag.text() recording_studio = tag.text() recording_studio_region = tag.pairs() recording_studio_address = tag.text() production_company = tag.text() production_company_region = tag.pairs() music_producer = tag.text() mixer = tag.text() primary_style = tag.text() # todo Introduce Recording bitrate = tag.numeric() duration = tag.decimal() def __init__(self, filename, metadata=None, chain_of_title=None): self.filename = filename self.metadata = metadata or Metadata() self.chain_of_title = chain_of_title or ChainOfTitle.from_track(self) @property def type(self): return self.album.type if self.album is not None else None def __repr__(self): return "Track(filename={}, metadata={})".format( self.filename, self.metadata) def update(self, **metadata): for key, value in metadata.items(): setattr(self, key, value) self.chain_of_title.update(lyricists=self.lyricist or [], composers=self.composer or [], publishers=self.publisher or [])
class FakeAudioPlayer(metaclass=Observable): playing = signal(Track) stopped = signal(Track) error_occurred = signal(Track, int) track = None def play(self, track): self.track = track self.playing.emit(self.track) def stop(self): if self.track is not None: self.stopped.emit(self.track) self.track = None def error(self, error): self.error_occurred.emit(self.track, error)
class AlbumPortfolio(metaclass=Observable): album_created = signal(Album) album_removed = signal(Album) def __init__(self): self._albums = [] def add_album(self, album): self._albums.append(album) self.album_created.emit(album) def remove_album(self, album): self._albums.remove(album) self.album_removed.emit(album) def __getitem__(self, index): return self._albums[index] def __len__(self): return len(self._albums)
class UserPreferences(metaclass=Observable): on_preferences_changed = signal(dict) artwork_location = locations.Pictures locale = "en" def __setattr__(self, name, value): super().__setattr__(name, value) self.on_preferences_changed.emit({name: value}) def __repr__(self): return "UserPreferences(locale={})".format(self.locale)
class Session(metaclass=Observable): user_signed_in = signal(User) user_signed_out = signal(User) _user = None def login_as(self, email, token, permissions): self._user = User.registered_as(email, token, permissions) self.user_signed_in.emit(self._user) def logout(self): logged_out, self._user = self._user, None self.user_signed_out.emit(logged_out) @property def opened(self): return self._user is not None @property def current_user(self): return self._user if self.opened else User.anonymous()
class ProjectStudio(metaclass=Observable): on_project_opened = signal(Album) on_project_saved = signal(Album) on_project_closed = signal(Album) _current_project = None @property def current_project(self): return self._current_project def project_loaded(self, project): self._current_project = project self.on_project_opened.emit(project) project_created = project_loaded def project_saved(self, project): self.on_project_saved.emit(project) def project_closed(self, project): self._current_project = None self.on_project_closed.emit(project)
class Name(QTableWidgetItem): on_name_changed = signal(str) def __init__(self, contributor): super().__init__() self.setData(Qt.UserRole, contributor) self.setText(contributor.name) def value_changed(self): name = self.text() contributor = self.data(Qt.UserRole) if contributor.name != name: contributor.name = name self.on_name_changed.emit(name)
class Share(QTableWidgetItem): on_share_changed = signal(Contributor) def __init__(self, contributor): super().__init__() self.setData(Qt.UserRole, contributor) self.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.setText(contributor.share) def value_changed(self): share = self.text() contributor = self.data(Qt.UserRole) if contributor.share != share: contributor.share = share self.on_share_changed.emit(contributor)
class ContributorsTable(Table): on_contributor_changed = signal(dict) def __init__(self, table): super().__init__(table) self._contributors = [] def _emit_contributor_changed(self, contributor): values = dict(name=contributor.name, share=contributor.share, affiliation=contributor.affiliation) if isinstance(contributor, AuthorComposer): values["publisher"] = contributor.publisher self.on_contributor_changed.emit(values)
class IPI(QTableWidgetItem): on_ipi_changed = signal(str, str) def __init__(self, contributor, lookup_ipi): super().__init__() self._lookup_ipi = lookup_ipi self.setData(Qt.UserRole, contributor) self.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.setText(contributor.ipi) def name_changed(self, name): contributor = self.data(Qt.UserRole) contributor.ipi = self._lookup_ipi(name) self.setText(contributor.ipi) def value_changed(self): ipi = self.text() contributor = self.data(Qt.UserRole) if contributor.ipi != ipi: contributor.ipi = ipi self.on_ipi_changed.emit(contributor.name, ipi)
class ChainOfTitle: changed = signal(Signal.SELF) def __init__(self, **chain_of_title): self._publishers = chain_of_title.get("publishers", {}) self._authors_composers = chain_of_title.get("authors_composers", {}) @classmethod def from_track(cls, track): chain = cls() chain.update(lyricists=track.lyricist or [], composers=track.composer or [], publishers=track.publisher or []) return chain @property def contributors(self): return { "authors_composers": self._authors_composers, "publishers": self._publishers } def update(self, lyricists, composers, publishers): has_updated_authors_composers = self._update_authors_composers( composers, lyricists) has_updated_publishers = self._update_publishers(publishers) if has_updated_authors_composers or has_updated_publishers: self.changed.emit(self) def update_contributor(self, **contributor): self._try_update_contributor(contributor, self._authors_composers) self._try_update_contributor(contributor, self._publishers) def _update_publishers(self, publishers): has_updated = self._update_contributors(publishers, self._publishers) if has_updated: self._update_associated_publishers(publishers) return has_updated def _update_associated_publishers(self, publishers): def publisher_has_been_removed(author_composer): return "publisher" in author_composer.keys( ) and author_composer["publisher"] not in publishers for contributor in self._authors_composers.values(): if publisher_has_been_removed(contributor): contributor["publisher"] = "" def _update_authors_composers(self, composers, lyricists): return self._update_contributors(lyricists + composers, self._authors_composers) @staticmethod def _update_contributors(new_contributors, contributors): to_remove = contributors.keys() - set(new_contributors) to_add = new_contributors - contributors.keys() for name in to_remove: del contributors[name] for name in to_add: contributors[name] = {"name": name} return len(to_remove) > 0 or len(to_add) > 0 @staticmethod def _try_update_contributor(contributor, contributors): contributor_name = contributor["name"] if contributor_name in contributors: contributors[contributor_name] = contributor
class Album(metaclass=tag.Taggable): track_inserted = signal(int, Track) track_removed = signal(int, Track) track_moved = signal(Track, int, int) # todo this should probably be in Track class Type: MP3 = "mp3" FLAC = "flac" release_name = tag.text() compilation = tag.flag() lead_performer = tag.text() lead_performer_region = tag.pairs() lead_performer_date_of_birth = tag.text() guest_performers = tag.pairs() label_name = tag.text() upc = tag.text() catalog_number = tag.text() release_time = tag.text() original_release_time = tag.text() contributors = tag.pairs() isnis = tag.map() ipis = tag.map() def __init__(self, metadata=None, of_type=Type.FLAC, filename=None): self.metadata = metadata.copy( *Album.tags()) if metadata is not None else Metadata() self.tracks = [] self.type = of_type self.filename = filename @property def images(self): return self.metadata.images def images_of_type(self, type_): return self.metadata.imagesOfType(type_) @property def main_cover(self): if not self.images: return None if self.front_covers: return self.front_covers[0] return self.images[0] @property def front_covers(self): return self.images_of_type(Image.FRONT_COVER) def add_image(self, mime, data, type_=Image.OTHER, desc=""): self.metadata.addImage(mime, data, type_, desc) self.metadata_changed.emit(self) def add_front_cover(self, mime, data, desc="Front Cover"): self.add_image(mime, data, Image.FRONT_COVER, desc) def remove_images(self): self.metadata.removeImages() self.metadata_changed.emit(self) def add_isni(self, name, id_): if self.isnis is None: self.isnis = {name: id_} else: self.isnis[name] = id_ self.metadata_changed.emit(self) def __len__(self): return len(self.tracks) def empty(self): return len(self) == 0 def add_track(self, track): self._insert_track(len(self.tracks), track) def insert_track(self, track, position): self._insert_track(position, track) def _insert_track(self, position, track): track.album = self # todo move to Track if not self.compilation: track.lead_performer = self.lead_performer self.tracks.insert(position, track) self._renumber_tracks() self.track_inserted.emit(position, track) def remove_track(self, position): track = self.tracks.pop(position) self._renumber_tracks() self.track_removed.emit(position, track) return track def move_track(self, from_position, to_position): track = self.tracks.pop(from_position) self.tracks.insert(to_position, track) self._renumber_tracks() self.track_moved.emit(track, from_position, to_position) def _renumber_tracks(self): for index, track in enumerate(self.tracks): track.track_number = index + 1 track.total_tracks = len(self.tracks)