def __init__(self, manager) -> None: super().__init__(manager, "wled") self.led_count = storage.get("wled_led_count") self.ip = storage.get("wled_ip") if not self.ip: try: device = util.get_devices()[0] broadcast = util.broadcast_of_device(device) self.ip = broadcast except Exception: # pylint: disable=broad-except # we don't want the startup to fail # just because the broadcast address could not be determined self.ip = "127.0.0.1" storage.put("wled_ip", self.ip) self.port = storage.get("wled_port") self.header = bytes([ 2, # DRGB protocol, we update every led every frame 1, # wait 1 second after the last packet until resuming normally ]) self.initialized = True redis.put("wled_initialized", True)
def request(self, session_key: str, archive: bool = True, manually_requested: bool = True) -> None: """Tries to request this resource. Uses the local cache if possible, otherwise tries to retrieve it online.""" if 0 < storage.get("max_queue_length") <= playback.queue.count(): self.error = "Queue limit reached" raise ProviderError(self.error) enqueue_function = enqueue if not self.check_cached(): if self.query is not None and storage.get("additional_keywords"): # add the additional keywords from the settings before checking self.query += " " + storage.get("additional_keywords") if not self.check_available(): raise ProviderError(self.error) # overwrite the enqueue function and make the resource available before calling it enqueue_function = fetch_enqueue if storage.get("new_music_only"): if self.was_requested_before(): self.error = "Only new music is allowed!" raise ProviderError(self.error) if storage.get("song_cooldown") != 0: if self.on_cooldown(): raise ProviderError(self.error) self.enqueue_placeholder(manually_requested) enqueue_function.delay(self, session_key, archive)
def __init__(self) -> None: self.loop_active: Optional[Event] = Event() self.devices = Devices(Ring(self), Strip(self), WLED(self), Screen(self)) # these settings are mirrored from the database, # because some of them are accessed multiple times per update. self.settings: Settings = { "ups": storage.get("ups"), "dynamic_resolution": storage.get("dynamic_resolution"), "program_speed": storage.get("program_speed"), "fixed_color": storage.get("fixed_color"), "last_fixed_color": storage.get("fixed_color"), } self.utilities = Utilities(Disabled(self), Cava(self), Alarm(self)) cava_installed = shutil.which("cava") is not None # a dictionary containing all led programs by their name led_programs: Dict[str, LedProgram] = { self.utilities.disabled.name: self.utilities.disabled } led_program_classes = [Fixed, Rainbow] if cava_installed: led_program_classes.append(Adaptive) for led_program_class in led_program_classes: led_program = led_program_class(self) led_programs[led_program.name] = led_program # a dictionary containing all screen programs by their name screen_programs: Dict[str, ScreenProgram] = { self.utilities.disabled.name: self.utilities.disabled } logo_loop = Video(self, "LogoLoop.mp4", loop=True) screen_programs[logo_loop.name] = logo_loop if cava_installed: for variant in sorted(Visualization.get_variants()): screen_programs[variant] = Visualization(self, variant) redis.put("led_programs", list(led_programs.keys())) redis.put("screen_programs", list(screen_programs.keys())) # a dictionary containing *all* programs by their name self.programs: Dict[str, LightProgram] = { **led_programs, **screen_programs } for device in self.devices: device.load_program() self.consumers_changed() self.listener = Thread(target=self.listen_for_changes) self.listener.start()
def __init__(self, manager, name) -> None: self.manager = manager self.name = name assert self.name in ["ring", "strip", "wled", "screen"] self.brightness = storage.get( cast(DeviceBrightness, f"{self.name}_brightness")) self.monochrome = storage.get( cast(DeviceMonochrome, f"{self.name}_monochrome")) self.initialized = False redis.put(cast(DeviceInitialized, f"{self.name}_initialized"), False) self.program: LightProgram = Disabled(manager)
def create(query: Optional[str] = None, key: Optional[int] = None) -> "PlaylistProvider": """Factory method to create a playlist provider. Both query and key need to be specified. Detects the type of provider needed and returns one of corresponding type.""" if query is None: logging.error( "archived playlist requested but no query given (key %s)", key) raise ValueError if key is None: logging.error("archived playlist requested but no key given") raise ValueError try: archived_playlist = ArchivedPlaylist.objects.get(id=key) except ArchivedPlaylist.DoesNotExist as error: logging.error("archived song requested for nonexistent key %s", key) raise ValueError from error playlist_type = song_utils.determine_playlist_type(archived_playlist) provider_class: Optional[Type[PlaylistProvider]] = None if playlist_type == "local": from core.musiq.local import LocalPlaylistProvider provider_class = LocalPlaylistProvider elif storage.get("youtube_enabled") and playlist_type == "youtube": from core.musiq.youtube import YoutubePlaylistProvider provider_class = YoutubePlaylistProvider elif storage.get("spotify_enabled") and playlist_type == "spotify": from core.musiq.spotify import SpotifyPlaylistProvider provider_class = SpotifyPlaylistProvider elif storage.get( "soundcloud_enabled") and playlist_type == "soundcloud": from core.musiq.soundcloud import SoundcloudPlaylistProvider provider_class = SoundcloudPlaylistProvider elif storage.get("jamendo_enabled") and playlist_type == "jamendo": from core.musiq.jamendo import JamendoPlaylistProvider provider_class = JamendoPlaylistProvider elif playlist_type == "playlog": # The playlist may contain various song types, but all of them will be archived. # We can use the local playlist provider to enqueue them. from core.musiq.local import LocalPlaylistProvider provider_class = LocalPlaylistProvider if not provider_class: raise NotImplementedError( f"No provider for given playlist: {query}, {key}") provider = provider_class(query, key) return provider
def create( query: Optional[str] = None, key: Optional[int] = None, external_url: Optional[str] = None, ) -> "SongProvider": """Factory method to create a song provider. Either (query and key) or external url need to be specified. Detects the type of provider needed and returns one of corresponding type.""" if key is not None: if query is None: logging.error( "archived song requested but no query given (key %s)", key) raise ValueError() try: archived_song = ArchivedSong.objects.get(id=key) except ArchivedSong.DoesNotExist as error: logging.error("archived song requested for nonexistent key %s", key) raise ValueError() from error external_url = archived_song.url if external_url is None: raise ValueError( "external_url was provided and could not be inferred from remaining attributes." ) provider_class: Optional[Type[SongProvider]] = None url_type = song_utils.determine_url_type(external_url) if url_type == "local": from core.musiq.local import LocalSongProvider provider_class = LocalSongProvider elif storage.get("youtube_enabled") and url_type == "youtube": from core.musiq.youtube import YoutubeSongProvider provider_class = YoutubeSongProvider elif storage.get("spotify_enabled") and url_type == "spotify": from core.musiq.spotify import SpotifySongProvider provider_class = SpotifySongProvider elif storage.get("soundcloud_enabled") and url_type == "soundcloud": from core.musiq.soundcloud import SoundcloudSongProvider provider_class = SoundcloudSongProvider elif storage.get("jamendo_enabled") and url_type == "jamendo": from core.musiq.jamendo import JamendoSongProvider provider_class = JamendoSongProvider if not provider_class: raise NotImplementedError( f"No provider for given song: {external_url}") if not query and external_url: query = external_url provider = provider_class(query, key) return provider
def web_client(self) -> OAuthClient: """Returns the web client if it was already created. If not, it is created using the spotify credentials from the database.""" if Spotify._web_client is None: client_id = storage.get(key="spotify_client_id") client_secret = storage.get(key="spotify_client_secret") Spotify._web_client = OAuthClient( base_url="https://api.spotify.com/v1", refresh_url="https://auth.mopidy.com/spotify/token", client_id=client_id, client_secret=client_secret, ) return Spotify._web_client
def persist(self, session_key: str, archive: bool = True) -> None: metadata = self.get_metadata() # Increase counter of song/playlist with transaction.atomic(): queryset = ArchivedSong.objects.filter( url=metadata["external_url"]) if queryset.count() == 0: initial_counter = 1 if archive else 0 assert metadata["external_url"] archived_song = ArchivedSong.objects.create( url=metadata["external_url"], artist=metadata["artist"], title=metadata["title"], duration=metadata["duration"], counter=initial_counter, cached=metadata["cached"], ) else: if archive: queryset.update(counter=F("counter") + 1) archived_song = queryset.get() if archive: ArchivedQuery.objects.get_or_create(song=archived_song, query=self.query) if storage.get("logging_enabled") and session_key: RequestLog.objects.create(song=archived_song, session_key=session_key)
def persist(self, session_key: str, archive: bool = True) -> None: if self.is_radio(): return assert self.id if self.title is None: logging.warning("Persisting a playlist with no title (id %s)", self.id) self.title = "" with transaction.atomic(): queryset = ArchivedPlaylist.objects.filter(list_id=self.id) if queryset.count() == 0: initial_counter = 1 if archive else 0 archived_playlist = ArchivedPlaylist.objects.create( list_id=self.id, title=self.title, counter=initial_counter) for index, url in enumerate(self.urls): PlaylistEntry.objects.create(playlist=archived_playlist, index=index, url=url) else: if archive: queryset.update(counter=F("counter") + 1) archived_playlist = queryset.get() if archive: ArchivedPlaylistQuery.objects.get_or_create( playlist=archived_playlist, query=self.query) if storage.get("logging_enabled") and session_key: RequestLog.objects.create(playlist=archived_playlist, session_key=session_key)
def fetch_metadata(self) -> bool: # in case of a radio playlist, restrict the number of songs that are downloaded assert self.id if self.is_radio(): self.ydl_opts["playlistend"] = storage.get("max_playlist_items") # radios are not viewable with the /playlist?list= url, # create a video watch url with the radio list query_url = ("https://www.youtube.com/watch?v=" + self.id[2:] + "&list=" + self.id) else: # if only given the id, yt-dlp returns an info dict resolving this id to a url. # we want to receive the playlist entries directly, so we query the playlist url query_url = "https://www.youtube.com/playlist?list=" + self.id try: with yt_dlp.YoutubeDL(self.ydl_opts) as ydl: info_dict = ydl.extract_info(query_url, download=False) except (yt_dlp.utils.ExtractorError, yt_dlp.utils.DownloadError) as error: self.error = error return False if info_dict["_type"] != "playlist" or "entries" not in info_dict: # query was not a playlist url -> search for the query assert False assert self.id == info_dict["id"] if "title" in info_dict: self.title = info_dict["title"] for entry in info_dict["entries"]: self.urls.append("https://www.youtube.com/watch?v=" + entry["id"]) assert self.key is None return True
def try_providers(session_key: str, providers: List[MusicProvider]) -> MusicProvider: """Goes through every given provider and tries to request its music. Returns the first provider that was successful with an empty error. If unsuccessful, return the last provider.""" fallback = False last_provider = providers[-1] provider = providers[0] for provider in providers: try: provider.request(session_key) # the current provider could provide the song, don't try the other ones break except ProviderError: # this provider cannot provide this song, use the next provider # if this was the last provider, show its error # in new music only mode, do not allow fallbacks if storage.get("new_music_only") or provider == last_provider: return provider fallback = True provider.error = "" if fallback: provider.ok_message += " (used fallback)" return provider
def check_not_too_large(self, size: Optional[float]) -> bool: """Returns whether the the given size is small enough in order for the song to be played.""" max_size = storage.get("max_download_size") * 1024 * 1024 if (max_size != 0 and not self.check_cached() and (size is not None and size > max_size)): self.error = "Song too long" return False return True
def _handle_program_request(device: str, request: WSGIRequest) -> HttpResponse: program, response = extract_value(request.POST) assert device in ["ring", "strip", "wled", "screen"] if program == storage.get(cast(DeviceProgram, f"{device}_program")): # the program doesn't change, return immediately return HttpResponse() set_program(device, program) return response
def check_cached(self) -> bool: if not self.id: # id could not be extracted from query, needs to be serched return False if storage.get("dynamic_embedded_stream"): # youtube streaming links need to be fetched each time the song is requested return False return os.path.isfile(self.get_path())
def enabled_platforms_by_priority() -> List[str]: """Returns a list of all available platforms, ordered by priority.""" # local music can only be searched explicitly by key and thus is last return [ platform for platform in ["spotify", "youtube", "soundcloud", "jamendo", "local"] if storage.get(cast(PlatformEnabled, f"{platform}_enabled")) ]
def update_mopidy_config(output: str) -> None: """Updates mopidy's config with the credentials stored in the database. If no config_file is given, the default one is used.""" if settings.DOCKER: # raveberry cannot restart mopidy in the docker setup return if output == "pulse": if storage.get("feed_cava") and shutil.which("cava"): output = "cava" else: output = "regular" spotify_username = storage.get("spotify_username") spotify_password = storage.get("spotify_password") spotify_client_id = storage.get("spotify_client_id") spotify_client_secret = storage.get("spotify_client_secret") soundcloud_auth_token = storage.get("soundcloud_auth_token") jamendo_client_id = storage.get("jamendo_client_id") subprocess.call([ "sudo", "/usr/local/sbin/raveberry/update_mopidy_config", output, spotify_username, spotify_password, spotify_client_id, spotify_client_secret, soundcloud_auth_token, jamendo_client_id, ]) restart_mopidy()
def set_lights_shortcut(request: WSGIRequest) -> HttpResponse: """Stores the current lights state and restores the previous one.""" value, response = extract_value(request.POST) should_enable = strtobool(value) is_enabled = (storage.get("ring_program") != "Disabled" or storage.get("wled_program") != "Disabled" or storage.get("strip_program") != "Disabled") if should_enable == is_enabled: return HttpResponse() if should_enable: for device in ["ring", "wled", "strip"]: set_program( device, storage.get(cast(DeviceProgram, f"last_{device}_program"))) else: for device in ["ring", "wled", "strip"]: set_program(device, "Disabled") return response
def _offline_playlist_suggestions(query: str) -> List[SuggestionResult]: results: List[SuggestionResult] = [] terms = query.split() remaining_playlists = ArchivedPlaylist.objects.prefetch_related("queries") # exclude radios from suggestions remaining_playlists = remaining_playlists.exclude( list_id__startswith="RD").exclude(list_id__contains="&list=RD") # we could have more strict types with TypedDicts, # but massaging both QuerySets into a single Type would require asserts/casts, # which have little performance impact but should be avoided in this critical codepath playlist_results: Iterable[Mapping[str, Any]] if settings.DEBUG: matching_playlists = remaining_playlists for term in terms: matching_playlists = matching_playlists.filter( Q(title__icontains=term) | Q(queries__query__icontains=term)) playlist_results = (matching_playlists.order_by("-counter").values( "id", "title", "counter").distinct()[:storage.get("number_of_suggestions")]) else: from django.contrib.postgres.search import TrigramWordSimilarity similar_playlists = remaining_playlists.annotate( title_similarity=TrigramWordSimilarity(query, "title"), query_similarity=TrigramWordSimilarity(query, "queries__query"), max_similarity=Greatest("title_similarity", "query_similarity"), ) playlist_results = ( similar_playlists.order_by("-max_similarity").values( "id", "title", "counter").distinct()[:storage.get("number_of_suggestions")]) for playlist in playlist_results: archived_playlist = ArchivedPlaylist.objects.get(id=playlist["id"]) result_dict: SuggestionResult = { "key": playlist["id"], "value": playlist["title"], "counter": playlist["counter"], "type": song_utils.determine_playlist_type(archived_playlist), } results.append(result_dict) return results
def extract_id(self) -> Optional[str]: """Tries to extract the id from the given query. Returns the id if possible, otherwise None""" if self.key is not None: try: archived_song = ArchivedSong.objects.get(id=self.key) return self.__class__.get_id_from_external_url( archived_song.url) except ArchivedSong.DoesNotExist: return None if self.query is not None: url_type = song_utils.determine_url_type(self.query) provider_class: Optional[Type[SongProvider]] = None if url_type == "local": from core.musiq.local import LocalSongProvider provider_class = LocalSongProvider if storage.get("youtube_enabled") and url_type == "youtube": from core.musiq.youtube import YoutubeSongProvider provider_class = YoutubeSongProvider if storage.get("spotify_enabled") and url_type == "spotify": from core.musiq.spotify import SpotifySongProvider provider_class = SpotifySongProvider if storage.get("soundcloud_enabled") and url_type == "soundcloud": from core.musiq.soundcloud import SoundcloudSongProvider provider_class = SoundcloudSongProvider if storage.get("jamendo_enabled") and url_type == "jamendo": from core.musiq.jamendo import JamendoSongProvider provider_class = JamendoSongProvider if provider_class is not None: return provider_class.get_id_from_external_url(self.query) try: archived_song = ArchivedSong.objects.get(url=self.query) return self.__class__.get_id_from_external_url( archived_song.url) except ArchivedSong.DoesNotExist: return None logging.error( "Can not extract id because neither key nor query are known") return None
def vote(request: WSGIRequest) -> HttpResponse: """Modify the vote-count of the given song by the given amount. If a song receives too many downvotes, it is removed.""" key_param = request.POST.get("key") amount_param = request.POST.get("amount") if key_param is None or amount_param is None: return HttpResponseBadRequest() key = int(key_param) amount = int(amount_param) if amount < -2 or amount > 2: return HttpResponseBadRequest() if storage.get("ip_checking") and not user_manager.try_vote( user_manager.get_client_ip(request), key, amount): return HttpResponseBadRequest("nice try") models.CurrentSong.objects.filter(queue_key=key).update(votes=F("votes") + amount) try: current_song = models.CurrentSong.objects.get() if (current_song.queue_key == key and current_song.votes <= -storage.get( # pylint: disable=invalid-unary-operand-type "downvotes_to_kick")): with playback.mopidy_command() as allowed: if allowed: PLAYER.playback.next() except models.CurrentSong.DoesNotExist: pass removed = playback.queue.vote( key, amount, -storage.get("downvotes_to_kick"), # pylint: disable=invalid-unary-operand-type ) # if we removed a song by voting, and it was added by autoplay, # we want it to be the new basis for autoplay if removed is not None: if not removed.manually_requested: playback.handle_autoplay(removed.external_url or removed.title) else: playback.handle_autoplay() musiq.update_state() return HttpResponse()
def adjust(self) -> None: """Updates resolutions and resets the current one. Needed after changing screens or hotplugging after booting without a connected screen.""" self.resolution = storage.get("initial_resolution") resolutions = list(reversed(sorted(self.list_resolutions()))) redis.put("resolutions", resolutions) # if unset, initialize with the highest resolution if self.resolution == (0, 0): storage.put("initial_resolution", resolutions[0]) self.resolution = resolutions[0] self.set_resolution(self.resolution)
def _decorator(request: WSGIRequest) -> HttpResponse: if storage.get( "interactivity" ) != storage.Interactivity.full_control and not user_manager.has_controls( request.user): return HttpResponseForbidden() response = func(request) musiq.update_state() if response is not None: return response return HttpResponse()
def _check_internet() -> None: host = storage.get("connectivity_host") if not host: redis.put("has_internet", False) return response = subprocess.call( ["ping", "-c", "1", "-W", "3", host], stdout=subprocess.DEVNULL ) if response == 0: redis.put("has_internet", True) else: redis.put("has_internet", False)
def submit_hashtag(request: WSGIRequest) -> HttpResponse: """Add the given hashtag to the database.""" hashtag = request.POST.get("hashtag") if hashtag is None or len(hashtag) == 0: return HttpResponseBadRequest() if hashtag[0] != "#": hashtag = "#" + hashtag models.Tag.objects.create(text=hashtag, active=storage.get("hashtags_active")) return HttpResponse()
def fetch_soundcloud() -> None: from core.musiq.soundcloud import Soundcloud soundcloud_suggestions = Soundcloud().get_search_suggestions(query) soundcloud_suggestions = soundcloud_suggestions[:storage.get( "soundcloud_suggestions")] with results_lock: for suggestion in soundcloud_suggestions: results.append({ "key": -1, "value": suggestion, "type": "soundcloud-online" })
def is_forbidden(string: str) -> bool: """Returns whether the given string should be filtered according to the forbidden keywords.""" # We can't access the variable in settings/basic.py # since we are in a static context without a reference to bes keywords = storage.get("forbidden_keywords") words = re.split(r"[,\s]+", keywords.strip()) # delete empty matches words = [word for word in words if word] for word in words: if re.search(word, string, re.IGNORECASE): return True return False
def fetch_jamendo() -> None: from core.musiq.jamendo import Jamendo jamendo_suggestions = Jamendo().get_search_suggestions(query) jamendo_suggestions = jamendo_suggestions[:storage.get( "jamendo_suggestions")] with results_lock: for suggestion in jamendo_suggestions: results.append({ "key": -1, "value": suggestion, "type": "jamendo-online" })
def fetch_youtube() -> None: from core.musiq.youtube import Youtube youtube_suggestions = Youtube().get_search_suggestions(query) youtube_suggestions = youtube_suggestions[:storage.get( "youtube_suggestions")] with results_lock: for suggestion in youtube_suggestions: results.append({ "key": -1, "value": suggestion, "type": "youtube-online" })
def load_program(self) -> None: """Load and activate this device's program from the database.""" assert self.name in ["ring", "strip", "wled", "screen"] program_name = storage.get( cast(DeviceBrightness, f"{self.name}_program")) # only enable if the device is initialized if self.initialized: self.program = self.manager.programs[program_name] else: self.program = self.manager.utilities.disabled self.program.use()
def fetch_spotify() -> None: from core.musiq.spotify import Spotify spotify_suggestions = Spotify().get_search_suggestions( query, suggest_playlist) spotify_suggestions = spotify_suggestions[:storage.get( "spotify_suggestions")] with results_lock: for suggestion, external_url in spotify_suggestions: results.append({ "key": external_url, "value": suggestion, "type": "spotify-online", })