def handle(self, *args, **options): from core.models import ArchivedSong from core.musiq.song_provider import SongProvider for song in ArchivedSong.objects.all(): try: provider = SongProvider.create(external_url=song.url) except NotImplementedError: # For this song a provider is necessary that is not available # e.g. the song was played before, but the provider was disabled continue cached = provider.check_cached() if cached: # sync the metadata in the database with the file system # _get_path is defined for localdrive and youtube, # the only two providers that may be cached from core.musiq.local import LocalSongProvider from core.musiq.youtube import YoutubeSongProvider assert isinstance(provider, (YoutubeSongProvider, LocalSongProvider)) metadata = song_utils.get_metadata(provider.get_path()) song.artist = metadata["artist"] song.title = metadata["title"] song.duration = metadata["duration"] song.cached = True else: # keep old data but store that the song is not cached song.cached = False song.save()
def request_radio(request: WSGIRequest) -> HttpResponse: """Endpoint to request radio for the current song.""" try: current_song = CurrentSong.objects.get() except CurrentSong.DoesNotExist: return HttpResponseBadRequest("Need a song to play the radio") provider = SongProvider.create(external_url=current_song.external_url) return provider.request_radio(request.session.session_key)
def __init__(self, base): self.base = base self.logger = logging.getLogger('raveberry') self.song_provider = SongProvider(self) self.queue = QueuedSong.objects self.placeholders = [] self.player = Player(self) self.player.start()
def enqueue(self) -> None: for index, external_url in enumerate(self.urls): if index == self.musiq.base.settings.basic.max_playlist_items: break # request every url in the playlist as their own url song_provider = SongProvider.create(self.musiq, external_url=external_url) try: song_provider.request("", archive=False, manually_requested=False) except ProviderError: continue if settings.DEBUG: # the sqlite database has problems if songs are pushed very fast # while a new song is taken from the queue. Add a delay to mitigate. time.sleep(1)
def request_radio(self, request: WSGIRequest) -> HttpResponse: """Endpoint to request radio for the current song.""" # only get ip on user requests if self.base.settings.basic.logging_enabled: request_ip, _ = ipware.get_client_ip(request) if request_ip is None: request_ip = "" else: request_ip = "" try: current_song = CurrentSong.objects.get() except CurrentSong.DoesNotExist: return HttpResponseBadRequest("Need a song to play the radio") provider = SongProvider.create(self, external_url=current_song.external_url) return provider.request_radio(request_ip)
def get_providers( query: str, key: Optional[int] = None, playlist: bool = False, preferred_platform: Optional[str] = None, ) -> List[MusicProvider]: """Returns a list of all available providers for the given query, ordered by priority. If a preferred platform is given, that provider will be first.""" if key is not None: # an archived entry was requested. # The key determines the Provider provider: MusicProvider if playlist: provider = PlaylistProvider.create(query, key) else: provider = SongProvider.create(query, key) if provider is None: raise ProviderError("No provider found for requested key") return [provider] providers: List[MusicProvider] = [] for platform in enabled_platforms_by_priority(): module = importlib.import_module(f"core.musiq.{platform}") if playlist: provider_class = getattr(module, f"{platform.title()}PlaylistProvider") else: provider_class = getattr(module, f"{platform.title()}SongProvider") try: provider = provider_class(query, key) if platform == preferred_platform: providers.insert(0, provider) else: providers.append(provider) except WrongUrlError: pass if not providers: raise ProviderError("No backend configured to handle your request.") return providers
def enqueue(self) -> None: for index, external_url in enumerate(self.urls): if index == storage.get("max_playlist_items"): break # request every url in the playlist as their own url try: song_provider = SongProvider.create(external_url=external_url) song_provider.request("", archive=False, manually_requested=False) except (ProviderError, NotImplementedError) as error: logging.warning( "Error while enqueuing url %s to playlist %s: %s", external_url, self.title, self.id, ) logging.exception(error) continue if settings.DEBUG: # the sqlite database has problems if songs are pushed very fast # while a new song is taken from the queue. Add a delay to mitigate. time.sleep(1)
def handle_autoplay(self, url: Optional[str] = None) -> None: """Checks whether to add a song by autoplay and does so if necessary. :param url: if given, this url is used to find the next autoplayed song. Otherwise, the current song is used.""" if self.musiq.controller.autoplay and models.QueuedSong.objects.count( ) == 0: if url is None: # if no url was specified, use the one of the current song try: current_song = models.CurrentSong.objects.get() url = current_song.external_url except ( models.CurrentSong.DoesNotExist, models.CurrentSong.MultipleObjectsReturned, ): return provider = SongProvider.create(self.musiq, external_url=url) try: suggestion = provider.get_suggestion() # The player loop is not restarted after error automatically. # As this function can raise several exceptions (it might do networking) # we catch every exception to make sure the loop keeps running except Exception as e: # pylint: disable=broad-except logging.exception("error during suggestions for %s: %s", url, e) else: self.musiq.do_request_music( "", suggestion, None, False, provider.type, archive=False, manually_requested=False, )
def _loop(self) -> None: """The main loop of the player. Takes a song from the queue and plays it until it is finished.""" while True: catch_up = None if models.CurrentSong.objects.exists(): # recover interrupted song from database current_song = models.CurrentSong.objects.get() # continue with the current song (approximately) where we last left song_provider = SongProvider.create( self.musiq, external_url=current_song.external_url) duration = int(song_provider.get_metadata()["duration"]) catch_up = round( (timezone.now() - current_song.created).total_seconds() * 1000) if catch_up > duration * 1000: catch_up = -1 else: self.queue_semaphore.acquire() if not self.running: break if self.backup_playing.is_set(): # stop backup stream self.backup_playing.clear() with self.mopidy_command(important=True) as allowed: if allowed: self.player.playback.next() # select the next song depending on settings song: Optional[models.QueuedSong] if self.musiq.base.settings.basic.voting_system: with transaction.atomic(): song = self.queue.confirmed().order_by( "-votes", "index")[0] song_id = song.id self.queue.remove(song.id) elif self.musiq.controller.shuffle: confirmed = self.queue.confirmed() index = random.randint(0, confirmed.count() - 1) song_id = confirmed[index].id song = self.queue.remove(song_id) else: # move the first song in the queue into the current song song_id, song = self.queue.dequeue() if song is None: # either the semaphore didn't match up with the actual count # of songs in the queue or a race condition occured logging.warning("dequeued on empty list") continue current_song = models.CurrentSong.objects.create( queue_key=song_id, manually_requested=song.manually_requested, votes=song.votes, internal_url=song.internal_url, external_url=song.external_url, artist=song.artist, title=song.title, duration=song.duration, ) self.handle_autoplay() try: archived_song = models.ArchivedSong.objects.get( url=current_song.external_url) votes: Optional[int] if self.musiq.base.settings.basic.voting_system: votes = current_song.votes else: votes = None if self.musiq.base.settings.basic.logging_enabled: models.PlayLog.objects.create( song=archived_song, manually_requested=current_song.manually_requested, votes=votes, ) except ( models.ArchivedSong.DoesNotExist, models.ArchivedSong.MultipleObjectsReturned, ): pass self.musiq.update_state() playing = Event() @self.player.on_event("playback_state_changed") def _on_playback_state_changed(_event): playing.set() with self.mopidy_command(important=True): # after a restart consume may be set to False again, so make sure it is on self.player.tracklist.clear() self.player.tracklist.set_consume(True) self.player.tracklist.add(uris=[current_song.internal_url]) self.player.playback.play() # mopidy can only seek when the song is playing playing.wait(timeout=1) if catch_up is not None and catch_up >= 0: self.player.playback.seek(catch_up) self.musiq.update_state() if catch_up is None or catch_up >= 0: if not self._wait_until_song_end(): # there was a ConnectionError during waiting for the song to end # we do not delete the current song but recover its state by restarting the loop continue current_song.delete() if self.musiq.controller.repeat: song_provider = SongProvider.create( self.musiq, external_url=current_song.external_url) self.queue.enqueue(song_provider.get_metadata(), False) self.queue_semaphore.release() if (self.musiq.base.user_manager.partymode_enabled() and random.random() < self.musiq.base.settings.basic.alarm_probability): self.alarm_playing.set() self.musiq.base.lights.alarm_started() self.musiq.update_state() with self.mopidy_command(important=True): self.player.tracklist.add(uris=[ "file://" + os.path.join(settings.BASE_DIR, "config/sounds/alarm.m4a") ]) self.player.playback.play() playing.clear() playing.wait(timeout=1) self._wait_until_song_end() self.musiq.base.lights.alarm_stopped() self.musiq.update_state() self.alarm_playing.clear() if not self.queue.exists( ) and self.musiq.base.settings.sound.backup_stream: self.backup_playing.set() # play backup stream self.player.tracklist.add( uris=[self.musiq.base.settings.sound.backup_stream]) self.player.playback.play() self.musiq.update_state()
def get_suggestions(self, request: WSGIRequest) -> JsonResponse: """Returns suggestions for a given query. Combines online and offline suggestions.""" query = request.GET["term"] suggest_playlist = request.GET["playlist"] == "true" if self.musiq.base.settings.basic.new_music_only and not suggest_playlist: return JsonResponse([], safe=False) results = self._online_suggestions(query, suggest_playlist) basic_settings = self.musiq.base.settings.basic if suggest_playlist: search_results = watson.search(query, models=( ArchivedPlaylist, ))[:basic_settings.number_of_suggestions] for playlist in search_results: playlist_info = playlist.meta archived_playlist = ArchivedPlaylist.objects.get( id=playlist_info["id"]) result_dict: Dict[str, Union[str, int]] = { "key": playlist_info["id"], "value": playlist_info["title"], "counter": playlist.object.counter, "type": song_utils.determine_playlist_type(archived_playlist), } results.append(result_dict) else: search_results = watson.search( query, models=(ArchivedSong, ))[:basic_settings.number_of_suggestions] for search_result in search_results: song_info = search_result.meta if song_utils.is_forbidden( self.musiq, song_info["artist"]) or song_utils.is_forbidden( self.musiq, song_info["title"]): continue try: provider = SongProvider.create( self.musiq, external_url=song_info["url"]) except NotImplementedError: # For this song a provider is necessary that is not available # e.g. the song was played before, but the provider was disabled continue cached = provider.check_cached() # don't suggest local songs if they are not cached (=not at expected location) if not cached and provider.type == "local": continue # don't suggest online songs when we don't have internet if not self.musiq.base.settings.basic.has_internet and not cached: continue # don't suggest youtube songs if it was disabled if (not self.musiq.base.settings.platforms.youtube_enabled and provider.type == "youtube"): continue # don't suggest spotify songs if we are not logged in if (not self.musiq.base.settings.platforms.spotify_enabled and provider.type == "spotify"): continue # don't suggest soundcloud songs if we are not logged in if (not self.musiq.base.settings.platforms.soundcloud_enabled and provider.type == "soundcloud"): continue result_dict = { "key": song_info["id"], "value": song_utils.displayname(song_info["artist"], song_info["title"]), "counter": search_result.object.counter, "type": provider.type, } results.append(result_dict) return JsonResponse(results, safe=False)