Showing preview only (1,603K chars total). Download the full file or copy to clipboard to get everything.
Repository: librespot-org/librespot
Branch: dev
Commit: 33bf3a77ed4b
Files: 660
Total size: 1.4 MB
Directory structure:
gitextract_cwkohdkk/
├── .devcontainer/
│ ├── Dockerfile
│ ├── Dockerfile.alpine
│ └── devcontainer.json
├── .dockerignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ ├── example/
│ │ └── prepare-release.event
│ ├── scripts/
│ │ └── bump-versions.sh
│ └── workflows/
│ ├── build.yml
│ ├── cross-compile.yml
│ ├── prepare-release.yml
│ ├── quality.yml
│ └── release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── COMPILING.md
├── CONTRIBUTING.md
├── Cargo.toml
├── Cross.toml
├── LICENSE
├── PUBLISHING.md
├── README.md
├── SECURITY.md
├── audio/
│ ├── Cargo.toml
│ └── src/
│ ├── decrypt.rs
│ ├── fetch/
│ │ ├── mod.rs
│ │ └── receive.rs
│ ├── lib.rs
│ └── range_set.rs
├── cache/
│ └── .gitignore
├── connect/
│ ├── Cargo.toml
│ ├── README.md
│ └── src/
│ ├── context_resolver.rs
│ ├── lib.rs
│ ├── model.rs
│ ├── shuffle_vec.rs
│ ├── spirc.rs
│ ├── state/
│ │ ├── context.rs
│ │ ├── handle.rs
│ │ ├── metadata.rs
│ │ ├── options.rs
│ │ ├── provider.rs
│ │ ├── restrictions.rs
│ │ ├── tracks.rs
│ │ └── transfer.rs
│ └── state.rs
├── contrib/
│ ├── Dockerfile
│ ├── Dockerfile.Rpi
│ ├── cross-compile-armv6hf/
│ │ ├── Dockerfile
│ │ └── docker-build.sh
│ ├── docker-build.sh
│ ├── event_handler_example.py
│ ├── librespot.service
│ └── librespot.user.service
├── core/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── src/
│ │ ├── apresolve.rs
│ │ ├── audio_key.rs
│ │ ├── authentication.rs
│ │ ├── cache.rs
│ │ ├── cdn_url.rs
│ │ ├── channel.rs
│ │ ├── component.rs
│ │ ├── config.rs
│ │ ├── connection/
│ │ │ ├── codec.rs
│ │ │ ├── handshake.rs
│ │ │ └── mod.rs
│ │ ├── date.rs
│ │ ├── dealer/
│ │ │ ├── manager.rs
│ │ │ ├── maps.rs
│ │ │ ├── mod.rs
│ │ │ ├── protocol/
│ │ │ │ └── request.rs
│ │ │ └── protocol.rs
│ │ ├── deserialize_with.rs
│ │ ├── diffie_hellman.rs
│ │ ├── error.rs
│ │ ├── file_id.rs
│ │ ├── http_client.rs
│ │ ├── lib.rs
│ │ ├── login5.rs
│ │ ├── mercury/
│ │ │ ├── mod.rs
│ │ │ ├── sender.rs
│ │ │ └── types.rs
│ │ ├── packet.rs
│ │ ├── proxytunnel.rs
│ │ ├── session.rs
│ │ ├── socket.rs
│ │ ├── spclient.rs
│ │ ├── spotify_id.rs
│ │ ├── spotify_uri.rs
│ │ ├── token.rs
│ │ ├── util.rs
│ │ └── version.rs
│ └── tests/
│ └── connect.rs
├── discovery/
│ ├── Cargo.toml
│ ├── examples/
│ │ ├── discovery.rs
│ │ └── discovery_group.rs
│ └── src/
│ ├── avahi.rs
│ ├── lib.rs
│ └── server.rs
├── docs/
│ ├── authentication.md
│ ├── connection.md
│ └── dealer.md
├── examples/
│ ├── README.md
│ ├── get_token.rs
│ ├── play.rs
│ ├── play_connect.rs
│ └── playlist_tracks.rs
├── metadata/
│ ├── Cargo.toml
│ └── src/
│ ├── album.rs
│ ├── artist.rs
│ ├── audio/
│ │ ├── file.rs
│ │ ├── item.rs
│ │ └── mod.rs
│ ├── availability.rs
│ ├── content_rating.rs
│ ├── copyright.rs
│ ├── episode.rs
│ ├── error.rs
│ ├── external_id.rs
│ ├── image.rs
│ ├── lib.rs
│ ├── lyrics.rs
│ ├── playlist/
│ │ ├── annotation.rs
│ │ ├── attribute.rs
│ │ ├── diff.rs
│ │ ├── item.rs
│ │ ├── list.rs
│ │ ├── mod.rs
│ │ ├── operation.rs
│ │ └── permission.rs
│ ├── request.rs
│ ├── restriction.rs
│ ├── sale_period.rs
│ ├── show.rs
│ ├── track.rs
│ ├── util.rs
│ └── video.rs
├── oauth/
│ ├── Cargo.toml
│ ├── examples/
│ │ ├── oauth_async.rs
│ │ └── oauth_sync.rs
│ └── src/
│ └── lib.rs
├── playback/
│ ├── Cargo.toml
│ └── src/
│ ├── audio_backend/
│ │ ├── alsa.rs
│ │ ├── gstreamer.rs
│ │ ├── jackaudio.rs
│ │ ├── mod.rs
│ │ ├── pipe.rs
│ │ ├── portaudio.rs
│ │ ├── pulseaudio.rs
│ │ ├── rodio.rs
│ │ ├── sdl.rs
│ │ └── subprocess.rs
│ ├── config.rs
│ ├── convert.rs
│ ├── decoder/
│ │ ├── mod.rs
│ │ ├── passthrough_decoder.rs
│ │ └── symphonia_decoder.rs
│ ├── dither.rs
│ ├── lib.rs
│ ├── local_file.rs
│ ├── mixer/
│ │ ├── alsamixer.rs
│ │ ├── mappings.rs
│ │ ├── mod.rs
│ │ └── softmixer.rs
│ ├── player.rs
│ └── symphonia_util.rs
├── protocol/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── proto/
│ │ ├── AdContext.proto
│ │ ├── AdDecisionEvent.proto
│ │ ├── AdError.proto
│ │ ├── AdEvent.proto
│ │ ├── AdRequestEvent.proto
│ │ ├── AdSlotEvent.proto
│ │ ├── AmazonWakeUpTime.proto
│ │ ├── AudioDriverError.proto
│ │ ├── AudioDriverInfo.proto
│ │ ├── AudioFileSelection.proto
│ │ ├── AudioOffliningSettingsReport.proto
│ │ ├── AudioRateLimit.proto
│ │ ├── AudioSessionEvent.proto
│ │ ├── AudioSettingsReport.proto
│ │ ├── AudioStreamingSettingsReport.proto
│ │ ├── BoomboxPlaybackInstrumentation.proto
│ │ ├── BrokenObject.proto
│ │ ├── CacheError.proto
│ │ ├── CachePruningReport.proto
│ │ ├── CacheRealmPruningReport.proto
│ │ ├── CacheRealmReport.proto
│ │ ├── CacheReport.proto
│ │ ├── ClientLocale.proto
│ │ ├── ColdStartupSequence.proto
│ │ ├── CollectionLevelDbInfo.proto
│ │ ├── CollectionOfflineControllerEmptyTrackList.proto
│ │ ├── ConfigurationApplied.proto
│ │ ├── ConfigurationFetched.proto
│ │ ├── ConfigurationFetchedNonAuth.proto
│ │ ├── ConnectCredentialsRequest.proto
│ │ ├── ConnectDeviceDiscovered.proto
│ │ ├── ConnectDialError.proto
│ │ ├── ConnectMdnsPacketParseError.proto
│ │ ├── ConnectPullFailure.proto
│ │ ├── ConnectTransferResult.proto
│ │ ├── ConnectionError.proto
│ │ ├── ConnectionInfo.proto
│ │ ├── ConnectionStateChange.proto
│ │ ├── DefaultConfigurationApplied.proto
│ │ ├── DesktopAuthenticationFailureNonAuth.proto
│ │ ├── DesktopAuthenticationSuccess.proto
│ │ ├── DesktopDeviceInformation.proto
│ │ ├── DesktopGPUAccelerationInfo.proto
│ │ ├── DesktopHighMemoryUsage.proto
│ │ ├── DesktopPerformanceIssue.proto
│ │ ├── DesktopUpdateDownloadComplete.proto
│ │ ├── DesktopUpdateDownloadError.proto
│ │ ├── DesktopUpdateMessageAction.proto
│ │ ├── DesktopUpdateMessageProcessed.proto
│ │ ├── DesktopUpdateResponse.proto
│ │ ├── Download.proto
│ │ ├── DrmRequestFailure.proto
│ │ ├── EndAd.proto
│ │ ├── EventSenderInternalErrorNonAuth.proto
│ │ ├── EventSenderStats.proto
│ │ ├── EventSenderStats2NonAuth.proto
│ │ ├── ExternalDeviceInfo.proto
│ │ ├── GetInfoFailures.proto
│ │ ├── HeadFileDownload.proto
│ │ ├── LegacyEndSong.proto
│ │ ├── LocalFileSyncError.proto
│ │ ├── LocalFilesError.proto
│ │ ├── LocalFilesImport.proto
│ │ ├── LocalFilesReport.proto
│ │ ├── LocalFilesSourceReport.proto
│ │ ├── MdnsLoginFailures.proto
│ │ ├── MetadataExtensionClientStatistic.proto
│ │ ├── Offline2ClientError.proto
│ │ ├── Offline2ClientEvent.proto
│ │ ├── OfflineError.proto
│ │ ├── OfflineEvent.proto
│ │ ├── OfflineReport.proto
│ │ ├── PlaybackError.proto
│ │ ├── PlaybackRetry.proto
│ │ ├── PlaybackSegments.proto
│ │ ├── PlayerStateRestore.proto
│ │ ├── PlaylistSyncEvent.proto
│ │ ├── PodcastAdSegmentReceived.proto
│ │ ├── Prefetch.proto
│ │ ├── PrefetchError.proto
│ │ ├── ProductStateUcsVerification.proto
│ │ ├── PubSubCountPerIdent.proto
│ │ ├── RawCoreStream.proto
│ │ ├── ReachabilityChanged.proto
│ │ ├── RejectedClientEventNonAuth.proto
│ │ ├── RemainingSkips.proto
│ │ ├── RequestAccounting.proto
│ │ ├── RequestTime.proto
│ │ ├── StartTrack.proto
│ │ ├── Stutter.proto
│ │ ├── TierFeatureFlags.proto
│ │ ├── TrackNotPlayed.proto
│ │ ├── TrackStuck.proto
│ │ ├── WindowSize.proto
│ │ ├── apiv1.proto
│ │ ├── app_state.proto
│ │ ├── audio_files_extension.proto
│ │ ├── audio_format.proto
│ │ ├── authentication.proto
│ │ ├── autodownload_backend_service.proto
│ │ ├── autodownload_config_common.proto
│ │ ├── autodownload_config_get_request.proto
│ │ ├── autodownload_config_set_request.proto
│ │ ├── automix_mode.proto
│ │ ├── autoplay_context_request.proto
│ │ ├── autoplay_node.proto
│ │ ├── canvas.proto
│ │ ├── canvas_storage.proto
│ │ ├── canvaz-meta.proto
│ │ ├── canvaz.proto
│ │ ├── capping_data.proto
│ │ ├── claas.proto
│ │ ├── client-tts.proto
│ │ ├── client_config.proto
│ │ ├── client_update.proto
│ │ ├── clips_cover.proto
│ │ ├── collection/
│ │ │ ├── album_collection_state.proto
│ │ │ ├── artist_collection_state.proto
│ │ │ ├── episode_collection_state.proto
│ │ │ ├── show_collection_state.proto
│ │ │ └── track_collection_state.proto
│ │ ├── collection2v2.proto
│ │ ├── collection_add_remove_items_request.proto
│ │ ├── collection_ban_request.proto
│ │ ├── collection_decoration_policy.proto
│ │ ├── collection_get_bans_request.proto
│ │ ├── collection_index.proto
│ │ ├── collection_item.proto
│ │ ├── collection_platform_items.proto
│ │ ├── collection_platform_requests.proto
│ │ ├── collection_platform_responses.proto
│ │ ├── concat_cosmos.proto
│ │ ├── connect.proto
│ │ ├── connectivity.proto
│ │ ├── contains_request.proto
│ │ ├── content_access_token_cosmos.proto
│ │ ├── context.proto
│ │ ├── context_application_desktop.proto
│ │ ├── context_client_id.proto
│ │ ├── context_device_desktop.proto
│ │ ├── context_index.proto
│ │ ├── context_installation_id.proto
│ │ ├── context_monotonic_clock.proto
│ │ ├── context_node.proto
│ │ ├── context_page.proto
│ │ ├── context_player_options.proto
│ │ ├── context_processor.proto
│ │ ├── context_sdk.proto
│ │ ├── context_time.proto
│ │ ├── context_track.proto
│ │ ├── context_view.proto
│ │ ├── context_view_cyclic_list.proto
│ │ ├── context_view_entry.proto
│ │ ├── context_view_entry_key.proto
│ │ ├── cosmos_changes_request.proto
│ │ ├── cosmos_decorate_request.proto
│ │ ├── cosmos_get_album_list_request.proto
│ │ ├── cosmos_get_artist_list_request.proto
│ │ ├── cosmos_get_episode_list_request.proto
│ │ ├── cosmos_get_show_list_request.proto
│ │ ├── cosmos_get_tags_info_request.proto
│ │ ├── cosmos_get_track_list_metadata_request.proto
│ │ ├── cosmos_get_track_list_request.proto
│ │ ├── cosmos_get_unplayed_episodes_request.proto
│ │ ├── cuepoints.proto
│ │ ├── decorate_request.proto
│ │ ├── devices.proto
│ │ ├── display_segments.proto
│ │ ├── display_segments_extension.proto
│ │ ├── entity_extension_data.proto
│ │ ├── es_add_to_queue_request.proto
│ │ ├── es_command_options.proto
│ │ ├── es_context.proto
│ │ ├── es_context_page.proto
│ │ ├── es_context_player_error.proto
│ │ ├── es_context_player_options.proto
│ │ ├── es_context_player_state.proto
│ │ ├── es_context_track.proto
│ │ ├── es_delete_session.proto
│ │ ├── es_get_error_request.proto
│ │ ├── es_get_play_history.proto
│ │ ├── es_get_position_state.proto
│ │ ├── es_get_queue_request.proto
│ │ ├── es_get_state_request.proto
│ │ ├── es_ident.proto
│ │ ├── es_ident_filter.proto
│ │ ├── es_logging_params.proto
│ │ ├── es_optional.proto
│ │ ├── es_pause.proto
│ │ ├── es_pauseresume_origin.proto
│ │ ├── es_play.proto
│ │ ├── es_play_options.proto
│ │ ├── es_play_origin.proto
│ │ ├── es_prefs.proto
│ │ ├── es_prepare_play.proto
│ │ ├── es_prepare_play_options.proto
│ │ ├── es_provided_track.proto
│ │ ├── es_pushed_message.proto
│ │ ├── es_queue.proto
│ │ ├── es_remote_config.proto
│ │ ├── es_request_info.proto
│ │ ├── es_response_with_reasons.proto
│ │ ├── es_restrictions.proto
│ │ ├── es_resume.proto
│ │ ├── es_seek_to.proto
│ │ ├── es_session_response.proto
│ │ ├── es_set_options.proto
│ │ ├── es_set_queue_request.proto
│ │ ├── es_set_repeating_context.proto
│ │ ├── es_set_repeating_track.proto
│ │ ├── es_set_shuffling_context.proto
│ │ ├── es_skip_next.proto
│ │ ├── es_skip_prev.proto
│ │ ├── es_skip_to_track.proto
│ │ ├── es_stop.proto
│ │ ├── es_storage.proto
│ │ ├── es_update.proto
│ │ ├── esperanto_options.proto
│ │ ├── event_entity.proto
│ │ ├── explicit_content_pubsub.proto
│ │ ├── extended_metadata.proto
│ │ ├── extension_descriptor_type.proto
│ │ ├── extension_kind.proto
│ │ ├── extracted_colors.proto
│ │ ├── follow_request.proto
│ │ ├── followed_users_request.proto
│ │ ├── frecency.proto
│ │ ├── frecency_storage.proto
│ │ ├── gabito.proto
│ │ ├── global_node.proto
│ │ ├── google/
│ │ │ └── protobuf/
│ │ │ ├── any.proto
│ │ │ ├── descriptor.proto
│ │ │ ├── duration.proto
│ │ │ ├── empty.proto
│ │ │ ├── field_mask.proto
│ │ │ ├── source_context.proto
│ │ │ ├── timestamp.proto
│ │ │ ├── type.proto
│ │ │ └── wrappers.proto
│ │ ├── greenroom_extension.proto
│ │ ├── identity.proto
│ │ ├── image-resolve.proto
│ │ ├── installation_data.proto
│ │ ├── instrumentation_params.proto
│ │ ├── keyexchange.proto
│ │ ├── lens-model.proto
│ │ ├── lfs_secret_provider.proto
│ │ ├── liked_songs_tags_sync_state.proto
│ │ ├── listen_later_cosmos_response.proto
│ │ ├── local_bans_storage.proto
│ │ ├── local_sync_cosmos.proto
│ │ ├── local_sync_state.proto
│ │ ├── logging_params.proto
│ │ ├── mdata.proto
│ │ ├── mdata_cosmos.proto
│ │ ├── mdata_storage.proto
│ │ ├── media.proto
│ │ ├── media_format.proto
│ │ ├── media_manifest.proto
│ │ ├── media_type.proto
│ │ ├── media_type_node.proto
│ │ ├── members_request.proto
│ │ ├── members_response.proto
│ │ ├── mercury.proto
│ │ ├── messages/
│ │ │ └── discovery/
│ │ │ ├── force_discover.proto
│ │ │ └── start_discovery.proto
│ │ ├── metadata/
│ │ │ ├── album_metadata.proto
│ │ │ ├── artist_metadata.proto
│ │ │ ├── episode_metadata.proto
│ │ │ ├── extension.proto
│ │ │ ├── image_group.proto
│ │ │ ├── show_metadata.proto
│ │ │ └── track_metadata.proto
│ │ ├── metadata.proto
│ │ ├── metadata_cosmos.proto
│ │ ├── metadata_esperanto.proto
│ │ ├── mod.rs
│ │ ├── modification_request.proto
│ │ ├── net-fortune.proto
│ │ ├── offline.proto
│ │ ├── offline_playlists_containing.proto
│ │ ├── on_demand_in_free_reason.proto
│ │ ├── on_demand_set_cosmos_request.proto
│ │ ├── on_demand_set_cosmos_response.proto
│ │ ├── on_demand_set_response.proto
│ │ ├── pause_resume_origin.proto
│ │ ├── pending_event_entity.proto
│ │ ├── perf_metrics_service.proto
│ │ ├── pin_request.proto
│ │ ├── play_history.proto
│ │ ├── play_origin.proto
│ │ ├── play_queue_node.proto
│ │ ├── play_reason.proto
│ │ ├── playback.proto
│ │ ├── playback_cosmos.proto
│ │ ├── playback_esperanto.proto
│ │ ├── playback_platform.proto
│ │ ├── playback_segments.proto
│ │ ├── playback_stack.proto
│ │ ├── playback_stack_v2.proto
│ │ ├── playback_state.proto
│ │ ├── played_state/
│ │ │ ├── episode_played_state.proto
│ │ │ ├── playability_restriction.proto
│ │ │ ├── show_played_state.proto
│ │ │ └── track_played_state.proto
│ │ ├── played_state.proto
│ │ ├── playedstate.proto
│ │ ├── player.proto
│ │ ├── player_license.proto
│ │ ├── player_model.proto
│ │ ├── playlist4_external.proto
│ │ ├── playlist_annotate3.proto
│ │ ├── playlist_contains_request.proto
│ │ ├── playlist_folder_state.proto
│ │ ├── playlist_get_request.proto
│ │ ├── playlist_members_request.proto
│ │ ├── playlist_modification_request.proto
│ │ ├── playlist_offline_request.proto
│ │ ├── playlist_permission.proto
│ │ ├── playlist_play_request.proto
│ │ ├── playlist_playback_request.proto
│ │ ├── playlist_playlist_state.proto
│ │ ├── playlist_query.proto
│ │ ├── playlist_request.proto
│ │ ├── playlist_set_base_permission_request.proto
│ │ ├── playlist_set_member_permission_request.proto
│ │ ├── playlist_set_permission_request.proto
│ │ ├── playlist_track_state.proto
│ │ ├── playlist_user_state.proto
│ │ ├── plugin.proto
│ │ ├── podcast_ad_segments.proto
│ │ ├── podcast_cta_cards.proto
│ │ ├── podcast_paywalls_cosmos.proto
│ │ ├── podcast_poll.proto
│ │ ├── podcast_qna.proto
│ │ ├── podcast_ratings.proto
│ │ ├── podcast_segments.proto
│ │ ├── podcast_segments_cosmos_request.proto
│ │ ├── podcast_segments_cosmos_response.proto
│ │ ├── podcast_subscription.proto
│ │ ├── podcast_virality.proto
│ │ ├── podcastextensions.proto
│ │ ├── policy/
│ │ │ ├── album_decoration_policy.proto
│ │ │ ├── artist_decoration_policy.proto
│ │ │ ├── episode_decoration_policy.proto
│ │ │ ├── folder_decoration_policy.proto
│ │ │ ├── playlist_album_decoration_policy.proto
│ │ │ ├── playlist_decoration_policy.proto
│ │ │ ├── playlist_episode_decoration_policy.proto
│ │ │ ├── playlist_request_decoration_policy.proto
│ │ │ ├── playlist_track_decoration_policy.proto
│ │ │ ├── rootlist_folder_decoration_policy.proto
│ │ │ ├── rootlist_playlist_decoration_policy.proto
│ │ │ ├── rootlist_request_decoration_policy.proto
│ │ │ ├── show_decoration_policy.proto
│ │ │ ├── supported_link_types_in_playlists.proto
│ │ │ ├── track_decoration_policy.proto
│ │ │ └── user_decoration_policy.proto
│ │ ├── popcount2_external.proto
│ │ ├── prepare_play_options.proto
│ │ ├── profile_cosmos.proto
│ │ ├── profile_service.proto
│ │ ├── property_definition.proto
│ │ ├── protobuf_delta.proto
│ │ ├── pubsub.proto
│ │ ├── queue.proto
│ │ ├── rate_limited_events.proto
│ │ ├── rcs.proto
│ │ ├── recently_played.proto
│ │ ├── recently_played_backend.proto
│ │ ├── record_id.proto
│ │ ├── remote.proto
│ │ ├── repeating_track_node.proto
│ │ ├── request_failure.proto
│ │ ├── resolve.proto
│ │ ├── resource_type.proto
│ │ ├── response_status.proto
│ │ ├── restrictions.proto
│ │ ├── resume_points_node.proto
│ │ ├── rootlist_request.proto
│ │ ├── seek_to_position.proto
│ │ ├── sequence_number_entity.proto
│ │ ├── session.proto
│ │ ├── set_member_permission_request.proto
│ │ ├── show_access.proto
│ │ ├── show_episode_state.proto
│ │ ├── show_offline_state.proto
│ │ ├── show_request.proto
│ │ ├── show_show_state.proto
│ │ ├── signal-model.proto
│ │ ├── skip_to_track.proto
│ │ ├── social_connect_v2.proto
│ │ ├── social_service.proto
│ │ ├── socialgraph_response_status.proto
│ │ ├── socialgraphv2.proto
│ │ ├── spirc.proto
│ │ ├── spotify/
│ │ │ ├── audiobookcashier/
│ │ │ │ └── v1/
│ │ │ │ └── audiobook_price.proto
│ │ │ ├── clienttoken/
│ │ │ │ └── v0/
│ │ │ │ └── clienttoken_http.proto
│ │ │ └── login5/
│ │ │ └── v3/
│ │ │ ├── challenges/
│ │ │ │ ├── code.proto
│ │ │ │ └── hashcash.proto
│ │ │ ├── client_info.proto
│ │ │ ├── credentials/
│ │ │ │ └── credentials.proto
│ │ │ ├── identifiers/
│ │ │ │ └── identifiers.proto
│ │ │ ├── login5.proto
│ │ │ └── user_info.proto
│ │ ├── state_restore/
│ │ │ ├── ads_rules_inject_tracks.proto
│ │ │ ├── automix_rules.proto
│ │ │ ├── automix_talk_rules.proto
│ │ │ ├── behavior_metadata_rules.proto
│ │ │ ├── circuit_breaker_rules.proto
│ │ │ ├── context_loader.proto
│ │ │ ├── context_player_restorable.proto
│ │ │ ├── context_player_rules.proto
│ │ │ ├── context_player_rules_base.proto
│ │ │ ├── context_player_state.proto
│ │ │ ├── explicit_content_rules.proto
│ │ │ ├── explicit_request_rules.proto
│ │ │ ├── kitteh_box_rules.proto
│ │ │ ├── mft_context_history.proto
│ │ │ ├── mft_context_switch_rules.proto
│ │ │ ├── mft_fallback_page_history.proto
│ │ │ ├── mft_rules.proto
│ │ │ ├── mft_rules_core.proto
│ │ │ ├── mft_rules_inject_filler_tracks.proto
│ │ │ ├── mft_state.proto
│ │ │ ├── mod_interruption_state.proto
│ │ │ ├── mod_rules_interruptions.proto
│ │ │ ├── music_injection_rules.proto
│ │ │ ├── playback_state.proto
│ │ │ ├── player_model.proto
│ │ │ ├── player_session.proto
│ │ │ ├── player_session_fake.proto
│ │ │ ├── player_session_queue.proto
│ │ │ ├── provided_track.proto
│ │ │ ├── random_source.proto
│ │ │ ├── remove_banned_tracks_rules.proto
│ │ │ ├── resume_points_rules.proto
│ │ │ └── track_error_rules.proto
│ │ ├── status.proto
│ │ ├── status_code.proto
│ │ ├── status_response.proto
│ │ ├── storage-resolve.proto
│ │ ├── storage_cosmos.proto
│ │ ├── storylines.proto
│ │ ├── stream_end_request.proto
│ │ ├── stream_handle.proto
│ │ ├── stream_progress_request.proto
│ │ ├── stream_seek_request.proto
│ │ ├── stream_start_request.proto
│ │ ├── stream_start_response.proto
│ │ ├── streaming_rule.proto
│ │ ├── suppressions.proto
│ │ ├── sync/
│ │ │ ├── album_sync_state.proto
│ │ │ ├── artist_sync_state.proto
│ │ │ ├── episode_sync_state.proto
│ │ │ └── track_sync_state.proto
│ │ ├── sync_request.proto
│ │ ├── techu_core_exercise_cosmos.proto
│ │ ├── track_instance.proto
│ │ ├── track_instantiator.proto
│ │ ├── transcripts.proto
│ │ ├── transfer_node.proto
│ │ ├── transfer_state.proto
│ │ ├── tts-resolve.proto
│ │ ├── ucs.proto
│ │ ├── unfinished_episodes_request.proto
│ │ ├── user_attributes.proto
│ │ ├── useraccount.proto
│ │ ├── your_library_config.proto
│ │ ├── your_library_contains_request.proto
│ │ ├── your_library_contains_response.proto
│ │ ├── your_library_decorate_request.proto
│ │ ├── your_library_decorate_response.proto
│ │ ├── your_library_decorated_entity.proto
│ │ ├── your_library_entity.proto
│ │ ├── your_library_index.proto
│ │ ├── your_library_pseudo_playlist_config.proto
│ │ ├── your_library_request.proto
│ │ └── your_library_response.proto
│ └── src/
│ ├── impl_trait/
│ │ ├── context.rs
│ │ └── player.rs
│ ├── impl_trait.rs
│ └── lib.rs
├── rust-toolchain.toml
├── rustfmt.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
│ └── player_event_handler.rs
└── test.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
# syntax=docker/dockerfile:1
ARG debian_version=slim-bookworm
ARG rust_version=1.85.0
FROM rust:${rust_version}-${debian_version}
ARG DEBIAN_FRONTEND=noninteractive
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse"
ENV RUST_BACKTRACE=1
ENV RUSTFLAGS="-D warnings"
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
nano\
openssh-server \
# for rust-analyzer vscode plugin
pkg-config \
# developer dependencies
libunwind-dev \
libpulse-dev \
portaudio19-dev \
libasound2-dev \
libsdl2-dev \
gstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libavahi-compat-libdnssd-dev && \
rm -rf /var/lib/apt/lists/*
RUN rustup component add rustfmt && \
rustup component add clippy && \
cargo install cargo-hack
================================================
FILE: .devcontainer/Dockerfile.alpine
================================================
# syntax=docker/dockerfile:1
ARG alpine_version=alpine3.20
ARG rust_version=1.85.0
FROM rust:${rust_version}-${alpine_version}
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse"
ENV RUST_BACKTRACE=1
ENV RUSTFLAGS="-D warnings -C target-feature=-crt-static"
RUN apk add --no-cache \
git \
nano\
openssh-server \
# for rust-analyzer vscode plugin
pkgconf \
musl-dev \
# developer dependencies
openssl-dev \
libunwind-dev \
pulseaudio-dev \
portaudio-dev \
alsa-lib-dev \
sdl2-dev \
gstreamer-dev \
gst-plugins-base-dev \
jack-dev \
avahi-dev && \
rm -rf /lib/apk/db/*
RUN rustup component add rustfmt && \
rustup component add clippy && \
cargo install cargo-hack
================================================
FILE: .devcontainer/devcontainer.json
================================================
{
"name": "Librespot Devcontainer",
"dockerFile": "Dockerfile.alpine",
"_postCreateCommand_comment": "Uncomment 'postCreateCommand' to run commands after the container is created.",
"_postCreateCommand": "",
"customizations": {
"_comment": "Configure properties specific to VS Code.",
"vscode": {
"settings": {
"dev.containers.copyGitConfig": true
},
"extensions": ["eamodio.gitlens", "github.vscode-github-actions", "rust-lang.rust-analyzer"]
}
},
"containerEnv": {
"GIT_EDITOR": "nano"
},
"_remoteUser_comment": "Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root",
"_remoteUser": "root"
}
================================================
FILE: .dockerignore
================================================
target
cache
protocol/target
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
### Look for similar bugs
Please check if there's [already an issue](https://github.com/librespot-org/librespot/issues) for your problem, and you're running at least the [latest release](https://github.com/librespot-org/librespot/releases/latest).
If you've only a "me too" comment to make, consider if a :+1: [reaction](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/)
will suffice.
### Description
A clear and concise description of what the problem is.
### Version
What version(s) of *librespot* does this problem exist in?
### How to reproduce
Steps to reproduce the behavior in *librespot* e.g.
1. Launch `librespot` with '...'
2. Connect with '...'
3. In the client click on '...'
4. See some error/problem
### Log
* A *full* **debug** log so we may trace your problem (launch `librespot` with `--verbose`).
* Ideally contains your above steps to reproduce.
* Format the log as code ([help](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks)) or use a *non-expiring* [pastebin](https://pastebin.com/).
* Redact data you consider personal but do not remove/trim anything else.
### Host (what you are running `librespot` on):
- OS: [e.g. Linux]
- Platform: [e.g. RPi 3B+]
### Additional context
Add any other context about the problem here. If your issue is related to sound playback, at a minimum specify the type and make of your output device.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: github-actions
schedule:
interval: weekly
day: saturday
time: "10:00"
groups:
gha-deps:
patterns:
- "*"
target-branch: dev
directories:
- "/.github/workflows"
================================================
FILE: .github/example/prepare-release.event
================================================
{
"action": "workflow_dispatch",
"inputs": {
"versionBump": "minor"
}
}
================================================
FILE: .github/scripts/bump-versions.sh
================================================
#!/usr/bin/env bash
# $fragment: see possible options https://github.com/christian-draeger/increment-semantic-version/tree/1.2.3?tab=readme-ov-file#version-fragment
if [ "$fragment" = "" ]; then
fragment=$1
fi
allowed_crates="protocol oauth core discovery audio metadata playback connect"
if [ "$fragment" = "patch" ]; then
last_tag=$(git describe --tags --abbrev=0)
awk_crates=$(echo "$allowed_crates" | tr ' ' '|')
diff_crates=$(git diff $last_tag... --stat --name-only \
| awk '/\.(rs|proto)$/{print}' \
| awk "/($awk_crates)\//{print}" \
| cut -d '/' -f 1 \
| uniq \
| tr \\n '\ ' \
| xargs )
echo "upgrading the following crates: [$diff_crates]"
else
diff_crates=$allowed_crates
echo "upgrading all crates for consistency"
fi
# append bin so that the version of the binary is also bumped
diff_crates="$diff_crates bin"
# required by script as it's usually a github action
export GITHUB_OUTPUT="version.txt"
# https://github.com/christian-draeger/increment-semantic-version/tree/1.2.3
increment_semver=$(curl https://raw.githubusercontent.com/christian-draeger/increment-semantic-version/refs/tags/1.2.3/entrypoint.sh)
for diff_crate in $diff_crates ; do
if [ "$diff_crate" = "bin" ]; then
toml="./Cargo.toml"
else
toml="./$diff_crate/Cargo.toml"
fi
from="$(cat $toml | awk "/version/{print; exit}" | cut -d\" -f 2)"
# execute script inline, extract result and remove output file
echo "$increment_semver" | bash /dev/stdin $from $fragment
to=$(cat $GITHUB_OUTPUT | cut -d= -f 2)
rm $GITHUB_OUTPUT
echo "upgrading [librespot-$diff_crate] from [$from] to [$to]"
# replace version in associated diff_crate toml
sed -i "0,/$from/{s/$from/$to/}" $toml
if [ "$diff_crate" = "bin" ]; then
continue
fi
# update workspace dependency in root toml
sed -i "/librespot-$diff_crate/{s/$from/$to/}" ./Cargo.toml
# update related dependencies in diff_crate
for allowed_crate in $allowed_crates ; do
cat ./$allowed_crate/Cargo.toml | grep librespot-$diff_crate > /dev/null
if [ $? = 0 ]; then
sed -i "/librespot-$diff_crate/{s/$from/$to/}" ./$allowed_crate/Cargo.toml
fi
done
done
exit 0
================================================
FILE: .github/workflows/build.yml
================================================
---
# Note, this is used in the badge URL!
name: build
"on":
push:
branches: [dev, master]
paths-ignore:
- "**.md"
- "docs/**"
- "contrib/**"
- "LICENSE"
- "*.sh"
- "**/Dockerfile*"
- "test.sh"
pull_request:
paths-ignore:
- "**.md"
- "docs/**"
- "contrib/**"
- "LICENSE"
- "*.sh"
- "**/Dockerfile*"
- "test.sh"
schedule:
# Run CI every week
- cron: "00 01 * * 0"
env:
RUST_BACKTRACE: 1
RUSTFLAGS: -D warnings
jobs:
test:
name: cargo +${{ matrix.toolchain }} test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
toolchain:
- "1.85" # MSRV (Minimum supported rust version)
- stable
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Install developer package dependencies (Linux)
if: runner.os == 'Linux'
run: >
sudo apt-get update && sudo apt-get install -y
libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev
gstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libavahi-compat-libdnssd-dev
- name: Fetch dependencies
run: cargo fetch --locked
- name: Build workspace with examples
run: cargo build --frozen --workspace --examples
- name: Run tests
run: cargo test --workspace
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Check packages without TLS requirements
run: cargo hack check -p librespot-protocol --each-feature
- name: Check workspace with native-tls
run: >
cargo hack check -p librespot --each-feature --exclude-all-features
--include-features native-tls
--exclude-features rustls-tls-native-roots,rustls-tls-webpki-roots
- name: Check workspace with rustls-tls-native-roots
run: >
cargo hack check -p librespot --each-feature --exclude-all-features
--include-features rustls-tls-native-roots
--exclude-features native-tls,rustls-tls-webpki-roots
- name: Check discovery features (Linux only)
if: runner.os == 'Linux'
run: >
cargo hack check -p librespot-discovery --each-feature --exclude-all-features
--include-features native-tls
--exclude-features rustls-tls-native-roots,rustls-tls-webpki-roots
- name: Build binary with default features
run: cargo build --frozen
- name: Upload debug artifacts
uses: actions/upload-artifact@v4
with:
name: librespot-${{ matrix.os }}-${{ matrix.toolchain }}
path: >
target/debug/librespot${{ runner.os == 'Windows' && '.exe' || '' }}
if-no-files-found: error
================================================
FILE: .github/workflows/cross-compile.yml
================================================
---
name: cross-compile
"on":
push:
branches: [dev, master]
paths-ignore:
- "**.md"
- "docs/**"
- "contrib/**"
- "LICENSE"
- "*.sh"
- "**/Dockerfile*"
pull_request:
paths-ignore:
- "**.md"
- "docs/**"
- "contrib/**"
- "LICENSE"
- "*.sh"
- "**/Dockerfile*"
env:
RUST_BACKTRACE: 1
RUSTFLAGS: -D warnings
jobs:
cross-compile:
name: cross +${{ matrix.toolchain }} build ${{ matrix.platform.target }}
runs-on: ${{ matrix.platform.runs-on }}
continue-on-error: false
strategy:
matrix:
platform:
- arch: armv7
runs-on: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
- arch: aarch64
runs-on: ubuntu-latest
target: aarch64-unknown-linux-gnu
- arch: riscv64gc
runs-on: ubuntu-latest
target: riscv64gc-unknown-linux-gnu
toolchain:
- "1.85" # MSRV (Minimum Supported Rust Version)
- stable
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Build binary with default features
if: matrix.platform.target != 'riscv64gc-unknown-linux-gnu'
uses: houseabsolute/actions-rust-cross@v1
with:
command: build
target: ${{ matrix.platform.target }}
toolchain: ${{ matrix.toolchain }}
args: --locked --verbose
- name: Build binary without system dependencies
if: matrix.platform.target == 'riscv64gc-unknown-linux-gnu'
uses: houseabsolute/actions-rust-cross@v1
with:
command: build
target: ${{ matrix.platform.target }}
toolchain: ${{ matrix.toolchain }}
args: --locked --verbose --no-default-features --features rustls-tls-webpki-roots
- name: Upload debug artifacts
uses: actions/upload-artifact@v4
with:
name: librespot-${{ matrix.platform.runs-on }}-${{ matrix.platform.arch }}-${{ matrix.toolchain }} # yamllint disable-line rule:line-length
path: target/${{ matrix.platform.target }}/debug/librespot
if-no-files-found: error
================================================
FILE: .github/workflows/prepare-release.yml
================================================
---
# test with
# act --job prepare-release --eventpath ./.github/example/prepare-release.event
name: prepare release
on:
workflow_dispatch:
inputs:
versionBump:
description: "Version bump for"
required: true
type: choice
options:
- major
- minor
- patch
jobs:
prepare-release:
name: Prepare release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-tags: true
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Bump versions
env:
fragment: ${{ github.event.inputs.versionBump }}
run: ./.github/scripts/bump-versions.sh
- name: Update Cargo.lock
run: cargo update --workspace
- name: Get binary version
id: get-version
run: |
VERSION=$(cat ./Cargo.toml | awk "/version/{print; exit}" | cut -d\" -f 2)
echo VERSION=$VERSION >> ${GITHUB_OUTPUT}
- name: Update Changelog
uses: thomaseizinger/keep-a-changelog-new-release@3.1.0
with:
tag: v${{ steps.get-version.outputs.VERSION }}
version: ${{ steps.get-version.outputs.VERSION }}
- name: Create PR to review automated changes
uses: peter-evans/create-pull-request@v7
if: ${{ !env.ACT }}
with:
commit-message: 'Preparations for v${{ steps.get-version.outputs.VERSION }}'
title: "Preparations for v${{ steps.get-version.outputs.VERSION }}"
branch: "prepare-release/v${{ steps.get-version.outputs.VERSION }}"
assignees: ${{ github.actor }}
body: |
This PR prepares for the next ${{ github.event.inputs.versionBump }} release v${{ steps.get-version.outputs.VERSION }}.
**Files that should be automatically modified:**
- `Cargo.toml` (version bump)
- `<crate>/Cargo.toml` (version bump)
> for patch versions only the necessary crates will receive an update
- `Cargo.lock` (only bumps own crate versions)
- `CHANGELOG.md` (finalize changelog for upcoming version)
**Review checklist:**
- [ ] Confirm the version bump in `Cargo.toml` is correct
- [ ] Ensure `Cargo.lock` did only update our crates
- [ ] Review the changelog for accuracy and completeness
Please verify these changes before merging.
---
After merging, continue by creating a new release with the tag `v${{ steps.get-version.outputs.VERSION }}`.
> See [PUBLISHING.md](https://github.com/librespot-org/librespot/blob/dev/PUBLISHING.md) for further infos.
================================================
FILE: .github/workflows/quality.yml
================================================
---
name: code-quality
"on":
push:
branches: [dev, master]
paths-ignore:
- "**.md"
- "docs/**"
- "contrib/**"
- "LICENSE"
- "*.sh"
- "**/Dockerfile*"
pull_request:
paths-ignore:
- "**.md"
- "docs/**"
- "contrib/**"
- "LICENSE"
- "*.sh"
- "**/Dockerfile*"
schedule:
# Run CI every week
- cron: "00 01 * * 0"
env:
RUST_BACKTRACE: 1
RUSTFLAGS: -D warnings
jobs:
fmt:
name: cargo fmt
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Check formatting
run: cargo fmt --all -- --check
clippy:
needs: fmt
name: cargo clippy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Install developer package dependencies
run: >
sudo apt-get update && sudo apt-get install -y
libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev
gstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libavahi-compat-libdnssd-dev
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Run clippy on packages without TLS requirements
run: cargo hack clippy -p librespot-protocol --each-feature
- name: Run clippy with native-tls
run: >
cargo hack clippy -p librespot --each-feature --exclude-all-features
--include-features native-tls
--exclude-features rustls-tls-native-roots,rustls-tls-webpki-roots
- name: Run clippy with rustls-tls-native-roots
run: >
cargo hack clippy -p librespot --each-feature --exclude-all-features
--include-features rustls-tls-native-roots
--exclude-features native-tls,rustls-tls-webpki-roots
- name: Run clippy with rustls-tls-webpki-roots
run: >
cargo hack clippy -p librespot --each-feature --exclude-all-features
--include-features rustls-tls-webpki-roots
--exclude-features native-tls,rustls-tls-native-roots
================================================
FILE: .github/workflows/release.yml
================================================
name: publish on release creation
on:
release:
types:
- created
workflow_dispatch:
jobs:
publish-crates:
name: Publish librespot
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libasound2-dev
- name: Verify librespot workspace
run: cargo publish --workspace --dry-run
- name: Publish librespot workspace
if: ${{ !env.ACT }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --workspace
================================================
FILE: .gitignore
================================================
target
.cargo
spotify_appkey.key
.idea/
.vagrant/
.project
.history
.cache
*.save
*.*~
================================================
FILE: .pre-commit-config.yaml
================================================
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: clippy
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0.
## [Unreleased]
### Added
- [connect] Add method `add_to_queue` to `Spirc` to add tracks, episodes, albums and playlists to the queue
- [playback] Add `SetQueue` player event, emitting when the queue changes (context loaded, track added to queue, or queue set via Spotify Connect). Gated behind `ConnectConfig::emit_set_queue_events`
### Changed
- [core] Made `SpotifyId::to_base62`, `SpotifyId::to_base16`, `FileId::to_base16`, `SpotifyUri::to_id`, `SpotifyUri::to_uri` infallible (breaking)
### Fixed
- [audio] Fixed integer overflow in throughput calculation
- [main] Fixed `--volume-ctrl fixed` not disabling volume control
- [core] Fix default permissions on credentials file and warn user if file is world readable
- [core] Try all resolved addresses for the dealer connection instead of failing after the first one.
## [0.8.0] - 2025-11-10
### Added
- [connect] Add method `transfer` to `Spirc` to automatically transfer the playback to ourselves
- [core] Add method `transfer` to `SpClient`
- [core] Add `SpotifyUri` type to represent more types of URI than `SpotifyId` can
- [discovery] Add support for [device aliases](https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf#device-aliases)
- [main] `--local-file-dir` / `-l` option added to binary to specify local file directories to pull from
- [metadata] `Local` variant added to `UniqueFields` enum (breaking)
- [playback] Local files can now be played with the following caveats:
- They must be sampled at 44,100 Hz
- They cannot be played from a Connect device using the dedicated 'Local Files' playlist; they must be added to another playlist first
- [playback] `local_file_directories` field added to `PlayerConfig` struct (breaking)
### Changed
- [contrib] Switched contrib/Dockerfile to new Debian stable (trixie)
- [core] `get_radio_for_track` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
- [core] Changed return type of `get_extended_metadata` to return `BatchedExtensionResponse` (breaking)
- [core] Changed parameter of `get_<item>_metadata` from `SpotifyId` to `SpotifyUri` (breaking)
- [metadata] Changed arguments for `Metadata` trait from `&SpotifyId` to `&SpotifyUri` (breaking)
- [playback] Changed type of `SpotifyId` fields in `PlayerEvent` members to `SpotifyUri` (breaking)
- [playback] `load` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
- [playback] `preload` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
### Fixed
- [connect] Fixed failed transferring with transfer data that had an empty context uri and no tracks
- [connect] Use the provided index or the first as fallback value to always play a track on loading
- [core] Fixed a problem where the metadata didn't include the audio file by switching to `get_extended_metadata`
- [core] Fixed connection issues after system suspend on Linux
### Removed
- [core] Removed `SpotifyItemType` enum; the new `SpotifyUri` is an enum over all item types and so which variant it is
describes its item type (breaking)
- [core] Removed `NamedSpotifyId` struct; it was made obsolete by `SpotifyUri` (breaking)
- [core] The following methods have been removed from `SpotifyId` and moved to `SpotifyUri` (breaking):
- `is_playable`
- `from_uri`
- `to_uri`
## [v0.7.1] - 2025-08-31
### Changed
- [connect] Shuffling was adjusted, so that shuffle and repeat can be used combined
### Fixed
- [core] Fix issue where building with native-tls would fail
- [connect] Repeat context will not go into autoplay anymore and triggering autoplay while shuffling shouldn't reshuffle anymore
- [connect] Only deletes the connect state on dealer shutdown instead on disconnecting
- [core] Fixed a problem where in `spclient` where an HTTP/411 error was thrown because the header was set wrong
- [main] Use the config instead of the type default for values that are not provided by the user
## [0.7.0] - 2025-08-24
### Changed
- [core] MSRV is now 1.85 with Rust edition 2024 (breaking)
- [core] AP connect and handshake have a combined 5 second timeout.
- [core] `stream_from_cdn` now accepts the URL as `TryInto<Uri>` instead of `CdnUrl` (breaking)
- [core] Add TLS backend selection with native-tls and rustls-tls options, defaulting to native-tls
- [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking)
- [connect] Changed `initial_volume` from `Option<u16>` to `u16` in `ConnectConfig` (breaking)
- [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking)
- [connect] Moved all public items to the highest level (breaking)
- [connect] Replaced Mercury usage in `Spirc` with Dealer
- [metadata] Replaced `AudioFileFormat` with own enum. (breaking)
- [playback] Changed trait `Mixer::open` to return `Result<Self, Error>` instead of `Self` (breaking)
- [playback] Changed type alias `MixerFn` to return `Result<Arc<dyn Mixer>, Error>` instead of `Arc<dyn Mixer>` (breaking)
- [playback] Optimize audio conversion to always dither at 16-bit level, and improve performance
- [playback] Normalizer maintains better stereo imaging, while also being faster
- [oauth] Remove loopback address requirement from `redirect_uri` when spawning callback handling server versus using stdin.
### Added
- [connect] Add command line parameter for setting volume steps.
- [connect] Add support for `seek_to`, `repeat_track` and `autoplay` for `Spirc` loading
- [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking)
- [connect] Add `volume_steps` to `ConnectConfig` (breaking)
- [connect] Add and enforce rustdoc
- [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking)
- [playback] Add `PlayerEvent::PositionChanged` event to notify about the current playback position
- [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient`
- [core] Add `try_get_urls` to `CdnUrl`
- [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process
### Fixed
- [test] Missing bindgen breaks crossbuild on recent runners. Now installing latest bindgen in addition.
- [core] Fix "no native root CA certificates found" on platforms unsupported
by `rustls-native-certs`.
- [core] Fix all APs rejecting with "TryAnotherAP" when connecting session
on Android platform.
- [core] Fix "Invalid Credentials" when using a Keymaster access token and
client ID on Android platform.
- [connect] Fix "play" command not handled if missing "offset" property
- [discovery] Fix libmdns zerconf setup errors not propagating to the main task.
- [metadata] `Show::trailer_uri` is now optional since it isn't always present (breaking)
- [metadata] Fix incorrect parsing of audio format
- [connect] Handle transfer of playback with empty "uri" field
- [connect] Correctly apply playing/paused state when transferring playback
- [player] Saturate invalid seek positions to track duration
- [audio] Fall back to other URLs in case of a failure when downloading from CDN
- [core] Metadata requests failing with 500 Internal Server Error
- [player] Rodio backend did not honor audio output format request
### Deprecated
- [oauth] `get_access_token()` function marked for deprecation
- [core] `try_get_url()` function marked for deprecation
### Removed
- [core] Removed `get_canvases` from SpClient (breaking)
- [core] DeviceType `homething` removed due to crashes on Android (breaking)
- [metadata] Removed `genres` from Album (breaking)
- [metadata] Removed `genre` from Artists (breaking)
## [0.6.0] - 2024-10-30
This version takes another step into the direction of the HTTP API, fixes a
couple of bugs, and makes it easier for developers to mock a certain platform.
Also it adds the option to choose avahi, dnssd or libmdns as your zeroconf
backend for Spotify Connect discovery.
### Changed
- [core] The `access_token` for http requests is now acquired by `login5`
- [core] MSRV is now 1.75 (breaking)
- [discovery] librespot can now be compiled with multiple MDNS/DNS-SD backends
(avahi, dns_sd, libmdns) which can be selected using a CLI flag. The defaults
are unchanged (breaking).
### Added
- [core] Add `get_token_with_client_id()` to get a token for a specific client ID
- [core] Add `login` (mobile) and `auth_token` retrieval via login5
- [core] Add `OS` and `os_version` to `config.rs`
- [discovery] Added a new MDNS/DNS-SD backend which connects to Avahi via D-Bus.
### Fixed
- [connect] Fixes initial volume showing zero despite playing in full volume instead
- [core] Fix "source slice length (16) does not match destination slice length
(20)" panic on some tracks
## [0.5.0] - 2024-10-15
This version is be a major departure from the architecture up until now. It
focuses on implementing the "new Spotify API". This means moving large parts
of the Spotify protocol from Mercury to HTTP. A lot of this was reverse
engineered before by @devgianlu of librespot-java. It was long overdue that we
started implementing it too, not in the least because new features like the
hopefully upcoming Spotify HiFi depend on it.
Splitting up the work on the new Spotify API, v0.5.0 brings HTTP-based file
downloads and metadata access. Implementing the "dealer" (replacing the current
Mercury-based SPIRC message bus with WebSockets, also required for social plays)
is a large and separate effort, slated for some later release.
While at it, we are taking the liberty to do some major refactoring to make
librespot more robust. Consequently not only the Spotify API changed but large
parts of the librespot API too. For downstream maintainers, we realise that it
can be a lot to move from the current codebase to this one, but believe us it
will be well worth it.
All these changes are likely to introduce new bugs as well as some regressions.
We appreciate all your testing and contributions to the repository:
<https://github.com/librespot-org/librespot>
### Changed
- [all] Assertions were changed into `Result` or removed (breaking)
- [all] Purge use of `unwrap`, `expect` and return `Result` (breaking)
- [all] `chrono` replaced with `time` (breaking)
- [all] `time` updated (CVE-2020-26235)
- [all] Improve lock contention and performance (breaking)
- [all] Use a single `player` instance. Eliminates occasional `player` and
`audio backend` restarts, which can cause issues with some playback
configurations.
- [all] Updated and removed unused dependencies
- [audio] Files are now downloaded over the HTTPS CDN (breaking)
- [audio] Improve file opening and seeking performance (breaking)
- [core] MSRV is now 1.74 (breaking)
- [connect] `DeviceType` moved out of `connect` into `core` (breaking)
- [connect] Update and expose all `spirc` context fields (breaking)
- [connect] Add `Clone, Defaut` traits to `spirc` contexts
- [connect] Autoplay contexts are now retrieved with the `spclient` (breaking)
- [contrib] Updated Docker image
- [core] Message listeners are registered before authenticating. As a result
there now is a separate `Session::new` and subsequent `session.connect`.
(breaking)
- [core] `ConnectConfig` moved out of `core` into `connect` (breaking)
- [core] `client_id` for `get_token` moved to `SessionConfig` (breaking)
- [core] Mercury code has been refactored for better legibility (breaking)
- [core] Cache resolved access points during runtime (breaking)
- [core] `FileId` is moved out of `SpotifyId`. For now it will be re-exported.
- [core] Report actual platform data on login
- [core] Support `Session` authentication with a Spotify access token
- [core] `Credentials.username` is now an `Option` (breaking)
- [core] `Session::connect` tries multiple access points, retrying each one.
- [core] Each access point connection now timesout after 3 seconds.
- [core] Listen on both IPV4 and IPV6 on non-windows hosts
- [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot`
now follows the setting in the Connect client that controls it. (breaking)
- [metadata] Most metadata is now retrieved with the `spclient` (breaking)
- [metadata] Playlists are moved to the `playlist4_external` protobuf (breaking)
- [metadata] Handle playlists that are sent with microsecond-based timestamps
- [playback] The audio decoder has been switched from `lewton` to `Symphonia`.
This improves the Vorbis sound quality, adds support for MP3 as well as for
FLAC in the future. (breaking)
- [playback] Improve reporting of actual playback cursor
- [playback] The passthrough decoder is now feature-gated (breaking)
- [playback] `rodio`: call play and pause
- [protocol] protobufs have been updated
### Added
- [all] Check that array indexes are within bounds (panic safety)
- [all] Wrap errors in librespot `Error` type (breaking)
- [audio] Make audio fetch parameters tunable
- [connect] Add option on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.
- [connect] Add session events
- [connect] Add `repeat`, `set_position_ms` and `set_volume` to `spirc.rs`
- [contrib] Add `event_handler_example.py`
- [core] Send metrics with metadata queries: client ID, country & product
- [core] Verify Spotify server certificates (prevents man-in-the-middle attacks)
- [core] User attributes are stored in `Session` upon login, accessible with a
getter and setter, and automatically updated as changes are pushed by the
Spotify infrastructure (breaking)
- [core] HTTPS is now supported, including for proxies (breaking)
- [core] Resolve `spclient` and `dealer` access points (breaking)
- [core] Get and cache tokens through new token provider (breaking)
- [core] `spclient` is the API for HTTP-based calls to the Spotify servers.
It supports a lot of functionality, including audio previews and image
downloads even if librespot doesn't use that for playback itself.
- [core] Support downloading of lyrics
- [core] Support parsing `SpotifyId` for local files
- [core] Support parsing `SpotifyId` for named playlists
- [core] Add checks and handling for stale server connections.
- [core] Fix potential deadlock waiting for audio decryption keys.
- [discovery] Add option to show playback device as a group
- [main] Add all player events to `player_event_handler.rs`
- [main] Add an event worker thread that runs async to the main thread(s) but
sync to itself to prevent potential data races for event consumers
- [metadata] All metadata fields in the protobufs are now exposed (breaking)
- [oauth] Standalone module to obtain Spotify access token using OAuth authorization code flow.
- [playback] Explicit tracks are skipped if the controlling Connect client has
disabled such content. Applications that use librespot as a library without
Connect should use the 'filter-explicit-content' user attribute in the session.
- [playback] Add metadata support via a `TrackChanged` event
- [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions
- [metadata] Add `Lyrics`
- [discovery] Add discovery initialisation retries if within the 1st min of uptime
### Fixed
- [connect] Set `PlayStatus` to the correct value when Player is loading to
avoid blanking out the controls when `self.play_status` is `LoadingPlay` or
`LoadingPause` in `spirc.rs`
- [connect] Handle attempts to play local files better by basically ignoring
attempts to load them in `handle_remote_update` in `spirc.rs`
- [connect] Loading previous or next tracks, or looping back on repeat, will
only start playback when we were already playing
- [connect, playback] Clean up and de-noise events and event firing
- [core] Fixed frequent disconnections for some users
- [core] More strict Spotify ID parsing
- [discovery] Update active user field upon connection
- [playback] Handle invalid track start positions by just starting the track
from the beginning
- [playback] Handle disappearing and invalid devices better
- [playback] Handle seek, pause, and play commands while loading
- [playback] Handle disabled normalisation correctly when using fixed volume
- [playback] Do not stop sink in gapless mode
- [metadata] Fix missing colon when converting named spotify IDs to URIs
## [0.4.2] - 2022-07-29
Besides a couple of small fixes, this point release is mainly to blacklist the
ap-gew4 and ap-gue1 access points that caused librespot to fail to playback
anything.
Development will now shift to the new HTTP-based API, targeted for a future
v0.5.0 release. The new-api branch will therefore be promoted to dev. This is a
major departure from the old API and although it brings many exciting new
things, it is also likely to introduce new bugs and some regressions.
Long story short, this v0.4.2 release is the most stable that librespot has yet
to offer. But, unless anything big comes up, it is also intended as the last
release to be based on the old API. Happy listening.
### Changed
- [playback] `pipe`: Better error handling
- [playback] `subprocess`: Better error handling
### Added
- [core] `apresolve`: Blacklist ap-gew4 and ap-gue1 access points that cause channel errors
- [playback] `pipe`: Implement stop
### Fixed
- [main] fix `--opt=value` line argument logging
- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa`
## [0.4.1] - 2022-05-23
This release fixes dependency issues when installing from crates.
### Changed
- [chore] The MSRV is now 1.56
### Fixed
- [playback] Fixed dependency issues when installing from crate
## [0.4.0] - 2022-05-21
Note: This version was yanked, because a corrupt package was uploaded and failed
to install.
This is a polishing release, adding a few little extras and improving on many
thers. We had to break a couple of API's to do so, and therefore bumped the
minor version number. v0.4.x may be the last in series before we migrate from
the current channel-based Spotify backend to a more HTTP-based backend.
Targeting that major effort for a v0.5 release sometime, we intend to maintain
v0.4.x as a stable branch until then.
### Changed
- [chore] The MSRV is now 1.53
- [contrib] Hardened security of the `systemd` service units
- [core] `Session`: `connect()` now returns the long-term credentials
- [core] `Session`: `connect()` now accepts a flag if the credentails should be stored via the cache
- [main] Different option descriptions and error messages based on what backends are enabled at build time
- [playback] More robust dynamic limiter for very wide dynamic range (breaking)
- [playback] `alsa`: improve `--device ?` output for the Alsa backend
- [playback] `gstreamer`: create own context, set correct states and use sync handler
- [playback] `pipe`: create file if it doesn't already exist
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking)
### Added
- [main] Enforce reasonable ranges for option values (breaking)
- [main] Add the ability to parse environment variables
- [main] Log now emits warning when trying to use options that would otherwise have no effect
- [main] Verbose logging now logs all parsed environment variables and command line arguments (credentials are redacted)
- [main] Add a `-q`, `--quiet` option that changes the logging level to WARN
- [main] Add `disable-credential-cache` flag (breaking)
- [main] Add a short name for every flag and option
- [playback] `pulseaudio`: set the PulseAudio name to match librespot's device name via `PULSE_PROP_application.name` environment variable (user set env var value takes precedence) (breaking)
- [playback] `pulseaudio`: set icon to `audio-x-generic` so we get an icon instead of a placeholder via `PULSE_PROP_application.icon_name` environment variable (user set env var value takes precedence) (breaking)
- [playback] `pulseaudio`: set values to: `PULSE_PROP_application.version`, `PULSE_PROP_application.process.binary`, `PULSE_PROP_stream.description`, `PULSE_PROP_media.software` and `PULSE_PROP_media.role` environment variables (user set env var values take precedence) (breaking)
### Fixed
- [connect] Don't panic when activating shuffle without previous interaction
- [core] Removed unsafe code (breaking)
- [main] Fix crash when built with Avahi support but Avahi is locally unavailable
- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given
- [main] Don't panic when parsing options, instead list valid values and exit
- [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`.
- [playback] Adhere to ReplayGain spec when calculating gain normalisation factor
- [playback] `alsa`: make `--volume-range` overrides apply to Alsa softvol controls
### Removed
- [playback] `alsamixer`: previously deprecated options `mixer-card`, `mixer-name` and `mixer-index` have been removed
## [0.3.1] - 2021-10-24
### Changed
- Include build profile in the displayed version information
- [playback] Improve dithering CPU usage by about 33%
### Fixed
- [connect] Partly fix behavior after last track of an album/playlist
## [0.3.0] - 2021-10-13
### Added
- [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`.
- [playback] Add support for dithering with `--dither` for lower requantization error (breaking)
- [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
- [playback] `alsamixer`: support for querying dB range from Alsa softvol
- [playback] Add `--format F64` (supported by Alsa and GStreamer only)
- [playback] Add `--normalisation-gain-type auto` that switches between album and track automatically
### Changed
- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)
- [audio, playback] Use `Duration` for time constants and functions (breaking)
- [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
- [connect] Synchronize player volume with mixer volume on playback
- [playback] Store and pass samples in 64-bit floating point
- [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic`
- [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking)
- [playback] `alsamixer`: complete rewrite (breaking)
- [playback] `alsamixer`: query card dB range for the volume control unless specified otherwise
- [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise
- [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking)
- [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink`
- [playback] `player`: update default normalisation threshold to -2 dBFS
- [playback] `player`: default normalisation type is now `auto`
### Deprecated
- [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate
- [playback] `alsamixer`: renamed `mixer-card` to `alsa-mixer-device`
- [playback] `alsamixer`: renamed `mixer-name` to `alsa-mixer-control`
- [playback] `alsamixer`: renamed `mixer-index` to `alsa-mixer-index`
### Removed
- [connect] Removed no-op mixer started/stopped logic (breaking)
- [playback] Removed `with-vorbis` and `with-tremor` features
- [playback] `alsamixer`: removed `--mixer-linear-volume` option, now that `--volume-ctrl {linear|log}` work as expected on Alsa
### Fixed
- [connect] Fix step size on volume up/down events
- [connect] Fix looping back to the first track after the last track of an album or playlist
- [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream
- [playback] Fix `log` and `cubic` volume controls to be mute at zero volume
- [playback] Fix `S24_3` format on big-endian systems
- [playback] `alsamixer`: make `cubic` consistent between cards that report minimum volume as mute, and cards that report some dB value
- [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected
- [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness
- [playback] `alsa`: revert buffer size to ~500 ms
- [playback] `alsa`, `pipe`, `pulseaudio`: better error handling
- [metadata] Skip tracks whose Spotify ID's can't be found (e.g. local files, which aren't supported)
## [0.2.0] - 2021-05-04
## [0.1.6] - 2021-02-22
## [0.1.5] - 2021-02-21
## [0.1.3] - 2020-07-29
## [0.1.2] - 2020-07-22
## [0.1.1] - 2020-01-30
## [0.1.0] - 2019-11-06
[unreleased]: https://github.com/librespot-org/librespot/compare/v0.8.0...HEAD
[0.8.0]: https://github.com/librespot-org/librespot/compare/v0.7.1...v0.8.0
[0.7.1]: https://github.com/librespot-org/librespot/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/librespot-org/librespot/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/librespot-org/librespot/compare/v0.4.2...v0.5.0
[0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6...v0.2.0
[0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5...v0.1.6
[0.1.5]: https://github.com/librespot-org/librespot/compare/v0.1.3...v0.1.5
[0.1.3]: https://github.com/librespot-org/librespot/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/librespot-org/librespot/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/librespot-org/librespot/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/librespot-org/librespot/releases/tag/v0.1.0
================================================
FILE: COMPILING.md
================================================
# Compiling
## Setup
In order to compile librespot, you will first need to set up a suitable Rust build environment, with the necessary dependencies installed. You will need to have a C compiler, Rust, and the development libraries for the audio backend(s) you want installed. These instructions will walk you through setting up a simple build environment.
### Install Rust
The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). Once that’s installed, Rust's standard tools should be set up and ready to use.
#### Additional Rust tools - `rustfmt`
To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) and [`clippy`](https://github.com/rust-lang/rust-clippy), which are installed by default with `rustup` these days, else they can be installed manually with:
```bash
rustup component add rustfmt
rustup component add clippy
```
Using `cargo fmt` and `cargo clippy` is not optional, as our CI checks against this repo's rules.
### General dependencies
Along with Rust, you will also require a C compiler.
On Debian/Ubuntu, install with:
```shell
sudo apt-get install build-essential
```
On Fedora systems, install with:
```shell
sudo dnf install gcc
```
### Audio library dependencies
Depending on the chosen backend, specific development libraries are required.
*_Note this is an non-exhaustive list, open a PR to add to it!_*
| Audio backend | Debian/Ubuntu | Fedora | macOS |
|--------------------|------------------------------|-----------------------------------|-------------|
|Rodio (default) | `libasound2-dev` | `alsa-lib-devel` | |
|ALSA | `libasound2-dev, pkg-config` | `alsa-lib-devel` | |
|GStreamer | `gstreamer1.0-plugins-base libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good libgstreamer-plugins-good1.0-dev` | `gstreamer1 gstreamer1-devel gstreamer1-plugins-base-devel gstreamer1-plugins-good` | `gstreamer gst-devtools gst-plugins-base gst-plugins-good` |
|PortAudio | `portaudio19-dev` | `portaudio-devel` | `portaudio` |
|PulseAudio | `libpulse-dev` | `pulseaudio-libs-devel` | |
|JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` |
|JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` |
|SDL | `libsdl2-dev` | `SDL2-devel` | `sdl2` |
|Pipe & subprocess | - | - | - |
###### For example, to build an ALSA based backend, you would need to run the following to install the required dependencies:
On Debian/Ubuntu:
```shell
sudo apt-get install libasound2-dev pkg-config
```
On Fedora systems:
```shell
sudo dnf install alsa-lib-devel
```
### Zeroconf library dependencies
Depending on the chosen backend, specific development libraries are required.
*_Note this is an non-exhaustive list, open a PR to add to it!_*
| Zeroconf backend | Debian/Ubuntu | Fedora | macOS |
|--------------------|------------------------------|-----------------------------------|-------------|
|avahi | | | |
|dns_sd | `libavahi-compat-libdnssd-dev pkg-config` | `avahi-compat-libdns_sd-devel` | |
|libmdns (default) | | | |
### TLS library dependencies
librespot requires a TLS implementation for secure connections to Spotify's servers. You can choose between two mutually exclusive options:
#### native-tls (default)
Uses your system's native TLS implementation:
- **Linux**: OpenSSL
- **macOS**: Secure Transport (Security.framework)
- **Windows**: SChannel (Windows TLS)
This is the **default choice** and provides the best compatibility. It integrates with your system's certificate store and is well-tested across platforms.
**When to choose native-tls:**
- You want maximum compatibility
- You're using system-managed certificates
- You're on a standard Linux distribution with OpenSSL
- You're deploying on platforms where OpenSSL is already present
**Dependencies:**
On Debian/Ubuntu:
```shell
sudo apt-get install libssl-dev pkg-config
```
On Fedora:
```shell
sudo dnf install openssl-devel pkg-config
```
#### rustls-tls
Uses a Rust-based TLS implementation with certificate authority (CA) verification. Two certificate store options are available:
**rustls-tls-native-roots**:
- **Linux**: Uses system ca-certificates package
- **macOS**: Uses Security.framework for CA verification
- **Windows**: Uses Windows certificate store
- Integrates with system certificate management and security updates
**rustls-tls-webpki-roots**:
- Uses Mozilla's compiled-in certificate store (webpki-roots)
- Certificate trust is independent of host system
- Best for reproducible builds, containers, or embedded systems
**When to choose rustls-tls:**
- You want to avoid external OpenSSL dependencies
- You're building for reproducible/deterministic builds
- You're targeting platforms where OpenSSL is unavailable or problematic (musl, embedded, static linking)
- You're cross-compiling and want to avoid OpenSSL build complexity
- You prefer having cryptographic operations implemented in Rust
**No additional system dependencies required** - rustls is implemented in Rust (with some assembly for performance-critical cryptographic operations) and doesn't require external libraries like OpenSSL.
#### Building with specific TLS backends
```bash
# Default (native-tls)
cargo build
# Explicitly use native-tls
cargo build --no-default-features --features "native-tls rodio-backend with-libmdns"
# Use rustls-tls with native certificate stores
cargo build --no-default-features --features "rustls-tls-native-roots rodio-backend with-libmdns"
# Use rustls-tls with Mozilla's webpki certificate store
cargo build --no-default-features --features "rustls-tls-webpki-roots rodio-backend with-libmdns"
```
**Important:** The TLS backends are mutually exclusive. Attempting to enable both will result in a compile-time error.
### Getting the Source
The recommended method is to first fork the repo, so that you have a copy that you have read/write access to. After that, it’s a simple case of cloning your fork.
```bash
git clone git@github.com:YOUR_USERNAME/librespot.git
```
## Compiling & Running
Once your build environment is setup, compiling the code is pretty simple.
### Compiling
To build a ```debug``` build with the default backend, from the project root run:
```bash
cargo build
```
And for ```release```:
```bash
cargo build --release
```
You will most likely want to build debug builds when developing, as they compile faster, and more verbose, and as the name suggests, are for the purposes of debugging. When submitting a bug report, it is recommended to use a debug build to capture stack traces.
There are also a number of compiler feature flags that you can add, in the event that you want to have certain additional features also compiled. All available features and their descriptions are documented in the main [Cargo.toml](Cargo.toml) file. Additional platform-specific information is available on the [wiki](https://github.com/librespot-org/librespot/wiki/Compiling#addition-features).
By default, librespot compiles with the ```native-tls```, ```rodio-backend```, and ```with-libmdns``` features.
**Note:** librespot requires at least one TLS backend to function. Building with `--no-default-features` alone will fail compilation. For custom feature selection, you must specify at least one TLS backend along with your desired audio and discovery backends.
For example, to build with the ALSA audio, libmdns discovery, and native-tls backends:
```bash
cargo build --no-default-features --features "native-tls alsa-backend with-libmdns"
```
Or to use rustls-tls with ALSA:
```bash
cargo build --no-default-features --features "rustls-tls alsa-backend with-libmdns"
```
### Compiling on Apple Silicon (M1+) for Apple x86_64
Install the additional `x86_64-apple-darwin` target using rustup:
```bash
rustup target install x86_64-apple-darwin
```
Then run the build with the additional target parameter:
```bash
cargo build --target=x86_64-apple-darwin --release
```
You can then use the `lipo` tool to create a single fat (universal) binary for both platforms:
```bash
lipo -create \
-arch x86_64 target/x86_64-apple-darwin/release/librespot \
-arch arm64 target/aarch64-apple-darwin/release/librespot \
-output librespot
```
### Running
Assuming you just compiled a ```debug``` build, you can run librespot with the following command:
```bash
./target/debug/librespot
```
There are various runtime options, documented in the wiki, and visible by running librespot with the ```-h``` argument.
Note that debug builds may cause buffer underruns and choppy audio when dithering is enabled (which it is by default). You can disable dithering with ```--dither none```.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
## Reporting an Issue
Issues are tracked in the Github issue tracker of the librespot repo.
If you have encountered a bug, please report it, as we rely on user reports to fix them.
Please also make sure that your issues are helpful. To ensure that your issue is helpful, please read over this brief checklist to avoid the more common pitfalls:
- Please take a moment to search/read previous similar issues to ensure you aren’t posting a duplicate. Duplicates will be closed immediately.
- Please include a clear description of what the issue is. Issues with descriptions such as ‘It hangs after 40 minutes’ will be closed immediately.
- Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues.
- Please be alert and respond to questions asked by any project members. Stale issues will be closed.
- When your issue concerns audio playback, please first make sure that your audio system is set up correctly and can play audio from other applications. This project aims to provide correct audio backends, not to provide Linux support to end users.
- Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run ‘export RUST_BACKTRACE=full’ before running librespot to enable backtraces.
## Contributing Code
If there is an issue that you would like to write a fix for, or a feature you would like to implement, we use the following flow for updating code in the librespot repo:
```
Fork -> Fix -> PR -> Review -> Merge
```
This is how all code is added to the repository, even by those with write access.
#### Steps before Committing
In order to prepare for a PR, you will need to do a couple of things first:
Make any changes that you are going to make to the code, but do not commit yet.
Unless your changes are negligible, please add an entry in the "Unreleased" section of `CHANGELOG.md`. Refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for instructions on how this entry should look like. If your changes break the API such that downstream packages that depend on librespot need to update their source to still compile, you should mark your changes as `(breaking)`.
Make sure that the code is correctly formatted by running:
```bash
cargo fmt --all
```
This command runs ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project:
```bash
cargo build
```
Once it has built, check for common code mistakes by running:
```bash
cargo clippy
```
Once you have confirmed there are no warnings or errors, you should commit your changes.
```bash
git commit -a -m "My fancy fix"
```
**N.B.** Please, for the sake of a readable history, do not bundle multiple major changes into a single commit. Instead, break it up into multiple commits.
Once you have made the commits you wish to have merged, push them to your forked repo:
```bash
git push
```
Then open a pull request on the main librespot repo.
Once a pull request is under way, it will be reviewed by one of the project maintainers, and either approved for merging, or have changes requested. Please be alert in the review period for possible questions about implementation decisions, implemented behaviour, and requests for changes. Once the PR is approved, it will be merged into the main repo.
Happy Contributing :)
================================================
FILE: Cargo.toml
================================================
[package]
name = "librespot"
version = "0.8.0"
rust-version.workspace = true
authors.workspace = true
license.workspace = true
description = "An open source client library for Spotify, with support for Spotify Connect"
keywords = ["audio", "spotify", "music", "streaming", "connect"]
categories = ["multimedia::audio"]
repository.workspace = true
readme = "README.md"
edition.workspace = true
include = [
"src/**/*",
"audio/**/*",
"connect/**/*",
"core/**/*",
"discovery/**/*",
"examples/**/*",
"metadata/**/*",
"oauth/**/*",
"playback/**/*",
"protocol/**/*",
"Cargo.toml",
"README.md",
"LICENSE",
"COMPILING.md",
"CONTRIBUTING.md",
]
[workspace.package]
rust-version = "1.85"
authors = ["Librespot Org"]
license = "MIT"
repository = "https://github.com/librespot-org/librespot"
edition = "2024"
[features]
default = ["native-tls", "rodio-backend", "with-libmdns"]
# TLS backends (mutually exclusive - compile-time checks in oauth/src/lib.rs)
# Note: Feature validation is in oauth crate since it's compiled first in the dependency tree.
# See COMPILING.md for more details on TLS backend selection.
# native-tls: Uses the system's native TLS stack (OpenSSL on Linux, Secure Transport on macOS,
# SChannel on Windows). This is the default as it's well-tested, widely compatible, and integrates
# with system certificate stores. Choose this for maximum compatibility and when you want to use
# system-managed certificates.
native-tls = ["librespot-core/native-tls", "librespot-oauth/native-tls"]
# rustls-tls: Uses the Rust-based rustls TLS implementation with certificate authority (CA)
# verification. This provides a Rust TLS stack (with assembly optimizations). Choose this for
# avoiding external OpenSSL dependencies, reproducible builds, or when targeting platforms where
# native TLS dependencies are unavailable or problematic (musl, embedded, static linking).
#
# Two certificate store options are available:
#
# - rustls-tls-native-roots: Uses rustls with native system certificate stores (ca-certificates on
# Linux, Security.framework on macOS, Windows certificate store on Windows). Best for most users as
# it integrates with system-managed certificates and gets security updates through the OS.
rustls-tls-native-roots = [
"librespot-core/rustls-tls-native-roots",
"librespot-oauth/rustls-tls-native-roots",
]
# rustls-tls-webpki-roots: Uses rustls with Mozilla's compiled-in certificate store (webpki-roots).
# Best for reproducible builds, containerized environments, or when you want certificate handling
# to be independent of the host system.
rustls-tls-webpki-roots = [
"librespot-core/rustls-tls-webpki-roots",
"librespot-oauth/rustls-tls-webpki-roots",
]
# Audio backends - see README.md for audio backend selection guide
# Cross-platform backends:
# rodio-backend: Cross-platform audio backend using Rodio (default). Provides good cross-platform
# compatibility with automatic backend selection. Uses ALSA on Linux, WASAPI on Windows, CoreAudio
# on macOS.
rodio-backend = ["librespot-playback/rodio-backend"]
# rodiojack-backend: Rodio backend with JACK support for professional audio setups.
rodiojack-backend = ["librespot-playback/rodiojack-backend"]
# gstreamer-backend: Uses GStreamer multimedia framework for audio output.
# Provides extensive audio processing capabilities.
gstreamer-backend = ["librespot-playback/gstreamer-backend"]
# portaudio-backend: Cross-platform audio I/O library backend.
portaudio-backend = ["librespot-playback/portaudio-backend"]
# sdl-backend: Simple DirectMedia Layer audio backend.
sdl-backend = ["librespot-playback/sdl-backend"]
# Platform-specific backends:
# alsa-backend: Advanced Linux Sound Architecture backend (Linux only).
# Provides low-latency audio output on Linux systems.
alsa-backend = ["librespot-playback/alsa-backend"]
# pulseaudio-backend: PulseAudio backend (Linux only).
# Integrates with the PulseAudio sound server for advanced audio routing.
pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
# jackaudio-backend: JACK Audio Connection Kit backend.
# Professional audio backend for low-latency, high-quality audio routing.
jackaudio-backend = ["librespot-playback/jackaudio-backend"]
# Network discovery backends - choose one for Spotify Connect device discovery
# See COMPILING.md for dependencies and platform support.
# with-libmdns: Pure-Rust mDNS implementation (default).
# No external dependencies, works on all platforms. Choose this for simple deployments or when
# avoiding system dependencies.
with-libmdns = ["librespot-discovery/with-libmdns"]
# with-avahi: Uses Avahi daemon for mDNS (Linux only).
# Integrates with system's Avahi service for network discovery. Choose this when you want to
# integrate with existing Avahi infrastructure or need advanced mDNS features. Requires
# libavahi-client-dev.
with-avahi = ["librespot-discovery/with-avahi"]
# with-dns-sd: Uses DNS Service Discovery (cross-platform).
# On macOS uses Bonjour, on Linux uses Avahi compatibility layer. Choose this for tight system
# integration on macOS or when using Avahi's dns-sd compatibility mode on Linux.
with-dns-sd = ["librespot-discovery/with-dns-sd"]
# Audio processing features:
# passthrough-decoder: Enables direct passthrough of Ogg Vorbis streams without decoding.
# Useful for custom audio processing pipelines or when you want to handle audio decoding
# externally. When enabled, audio is not decoded by librespot but passed through as raw Ogg Vorbis
# data.
passthrough-decoder = ["librespot-playback/passthrough-decoder"]
[lib]
name = "librespot"
path = "src/lib.rs"
[[bin]]
name = "librespot"
path = "src/main.rs"
doc = false
[workspace.dependencies]
librespot-audio = { version = "0.8.0", path = "audio", default-features = false }
librespot-connect = { version = "0.8.0", path = "connect", default-features = false }
librespot-core = { version = "0.8.0", path = "core", default-features = false }
librespot-discovery = { version = "0.8.0", path = "discovery", default-features = false }
librespot-metadata = { version = "0.8.0", path = "metadata", default-features = false }
librespot-oauth = { version = "0.8.0", path = "oauth", default-features = false }
librespot-playback = { version = "0.8.0", path = "playback", default-features = false }
librespot-protocol = { version = "0.8.0", path = "protocol", default-features = false }
[dependencies]
librespot-audio.workspace = true
librespot-connect.workspace = true
librespot-core.workspace = true
librespot-discovery.workspace = true
librespot-metadata.workspace = true
librespot-oauth.workspace = true
librespot-playback.workspace = true
librespot-protocol.workspace = true
data-encoding = "2.5"
env_logger = { version = "0.11.2", default-features = false, features = [
"color",
"humantime",
"auto-color",
] }
futures-util = { version = "0.3", default-features = false }
getopts = "0.2"
log = "0.4"
sha1 = "0.10"
sysinfo = { version = "0.36", default-features = false, features = ["system"] }
thiserror = "2"
tokio = { version = "1", features = [
"rt",
"macros",
"signal",
"sync",
"process",
] }
url = "2.2"
[package.metadata.deb]
maintainer = "Librespot Organization <noreply@github.com>"
copyright = "2015, Paul Liétar"
license-file = ["LICENSE", "4"]
depends = "$auto"
recommends = "avahi-daemon"
extended-description = """\
librespot is an open source client library for Spotify. It enables applications \
to use Spotify's service to control and play music via various backends, and to \
act as a Spotify Connect receiver. It is an alternative to the official and now \
deprecated closed-source libspotify. Additionally, it provides extra features \
which are not available in the official library.
.
This package provides the librespot binary for headless Spotify Connect playback. \
.
Note: librespot only works with Spotify Premium accounts."""
section = "sound"
priority = "optional"
assets = [
# Main binary
["target/release/librespot", "usr/bin/", "755"],
# Documentation
["README.md", "usr/share/doc/librespot/", "644"],
# Systemd services
["contrib/librespot.service", "lib/systemd/system/", "644"],
["contrib/librespot.user.service", "lib/systemd/user/", "644"],
]
[workspace.lints]
clippy.redundant_closure_for_method_calls = "warn"
[lints]
workspace = true
================================================
FILE: Cross.toml
================================================
[build]
pre-build = [
"dpkg --add-architecture $CROSS_DEB_ARCH",
"apt-get update",
"apt-get --assume-yes install libssl-dev:$CROSS_DEB_ARCH libasound2-dev:$CROSS_DEB_ARCH",
]
[target.riscv64gc-unknown-linux-gnu]
# RISC-V: Uses rustls-tls (no system dependencies needed)
# Building with --no-default-features --features rustls-tls
# No pre-build steps required - rustls is pure Rust
pre-build = []
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Paul Lietar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: PUBLISHING.md
================================================
# Publishing
## How To
1. [prepare the release](#prepare-the-release)
2. [create a github-release](#creating-a-github-release)
### Prepare the release
For preparing the release a manuel workflow should be available that takes care of the common preparation. But
this can also be done manually if so desired. The workflow does:
- upgrade the version according to the targeted release (`major`, `minor`, `patch`)
- `major` and `minor` require all crates to be updated
- `patch` instead only upgrades the crates that had any changes
- updates the changelog according to Keep-A-Changelog convention
- commits and pushes the changes to remote
### Creating a github-release
After everything is prepared for the new version. A [new release can be created](https://github.com/librespot-org/librespot/releases/new)
from the ui. The tag will not be available as it isn't set by the prepare workflow, so a new tag needs to be created.
The tag and name of the release should be named like `v<version>` where `version` is the version of the binary to be
published. As release notes, copy the entries from the changelog for this release.
The release should be created as draft, which will trigger the workflow that will publish the changed crates and binary.
The workflow will:
- check if all crates needs to be published or only certain crates
- publish the crates in a specific order while excluding crates that didn't have any changes
- publish the binary
After the workflow was successful the version can be published.
## Notes
Publishing librespot to crates.io is a slightly convoluted affair due to the various dependencies that each package has
on other local packages. The order of publishing that has been found to work is as follows:
> `protocol -> core -> audio -> metadata -> playback -> connect -> librespot`
The `protocol` package needs to be published with `cargo publish --no-verify` due to the build script modifying the
source during compile time. Publishing can be done using the command `cargo publish` in each of the directories of the
respective crate.
================================================
FILE: README.md
================================================
[](https://github.com/librespot-org/librespot/actions)
[](https://gitter.im/librespot-org/spotify-connect-resources)
[](https://crates.io/crates/librespot)
Current maintainers are [listed on GitHub](https://github.com/orgs/librespot-org/people).
# librespot
*librespot* is an open source client library for Spotify. It enables applications to use Spotify's service to control and play music via various backends, and to act as a Spotify Connect receiver. It is an alternative to the official and [now deprecated](https://pyspotify.mopidy.com/en/latest/#libspotify-s-deprecation) closed-source `libspotify`. Additionally, it will provide extra features which are not available in the official library.
_Note: librespot only works with Spotify Premium. This will remain the case. We will not support any features to make librespot compatible with free accounts, such as limited skips and adverts._
## Quick start
We're available on [crates.io](https://crates.io/crates/librespot) as the _librespot_ package. Simply run `cargo install librespot` to install librespot on your system. Check the wiki for more info and possible [usage options](https://github.com/librespot-org/librespot/wiki/Options).
After installation, you can run librespot from the CLI using a command such as `librespot -n "Librespot Speaker" -b 160` to create a speaker called _Librespot Speaker_ serving 160 kbps audio.
## This fork
As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future.
# Documentation
Documentation is currently a work in progress, contributions are welcome!
There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder.
[COMPILING.md](https://github.com/librespot-org/librespot/blob/master/COMPILING.md) contains detailed instructions on setting up a development environment, and compiling librespot. More general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki).
[CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains our contributing guidelines.
If you wish to learn more about how librespot works overall, the best way is to simply read the code, and ask any questions you have in our [Gitter Room](https://gitter.im/librespot-org/spotify-connect-resources).
# Issues & Discussions
**We have recently started using Github discussions for general questions and feature requests, as they are a more natural medium for such cases, and allow for upvoting to prioritize feature development. Check them out [here](https://github.com/librespot-org/librespot/discussions). Bugs and issues with the underlying library should still be reported as issues.**
If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, e.g. the Spotify URI of the song that caused the crash.
# Building
A quick walkthrough of the build process is outlined below, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md).
## Additional Dependencies
We recently switched to using [Rodio](https://github.com/tomaka/rodio) for audio playback by default, hence for macOS and Windows, you should just be able to clone and build librespot (with the command below).
For Linux, you will need to run the additional commands below, depending on your distro.
On Debian/Ubuntu, the following command will install these dependencies:
```shell
sudo apt-get install build-essential libasound2-dev
```
On Fedora systems, the following command will install these dependencies:
```shell
sudo dnf install alsa-lib-devel make gcc
```
librespot currently offers the following selection of [audio backends](https://github.com/librespot-org/librespot/wiki/Audio-Backends):
```
Rodio (default)
ALSA
GStreamer
PortAudio
PulseAudio
JACK
JACK over Rodio
SDL
Pipe
Subprocess
```
Please check [COMPILING.md](COMPILING.md) for detailed information on TLS, audio, and discovery backend dependencies, or the [Compiling](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) entry on the wiki for additional backend specific dependencies.
Once you've installed the dependencies and cloned this repository you can build *librespot* with the default features using Cargo.
```shell
cargo build --release
```
By default, this builds with native-tls (system TLS), rodio audio backend, and libmdns discovery. See [COMPILING.md](COMPILING.md) for information on selecting different TLS, audio, and discovery backends.
# Packages
librespot is also available via official package system on various operating systems such as Linux, FreeBSD, NetBSD. [Repology](https://repology.org/project/librespot/versions) offers a good overview.
[](https://repology.org/project/librespot/versions)
## Usage
A sample program implementing a headless Spotify Connect receiver is provided.
Once you've built *librespot*, run it using :
```shell
target/release/librespot --name DEVICENAME
```
The above is a minimal example. Here is a more fully fledged one:
```shell
target/release/librespot -n "Librespot" -b 320 -c ./cache --enable-volume-normalisation --initial-volume 75 --device-type avr
```
The above command will create a receiver named ```Librespot```, with bitrate set to 320 kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials.
A full list of runtime options is available [here](https://github.com/librespot-org/librespot/wiki/Options).
_Please Note: When using the cache feature, an authentication blob is stored for your account in the cache directory. For security purposes, we recommend that you set directory permissions on the cache directory to `700`._
## Contact
Come and hang out on gitter if you need help or want to offer some:
https://gitter.im/librespot-org/spotify-connect-resources
## Disclaimer
Using this code to connect to Spotify's API is probably forbidden by them.
Use at your own risk.
## License
Everything in this repository is licensed under the MIT license.
## Related Projects
This is a non exhaustive list of projects that either use or have modified librespot. If you'd like to include yours, submit a PR.
- [librespot-golang](https://github.com/librespot-org/librespot-golang) - A golang port of librespot.
- [plugin.audio.spotify](https://github.com/marcelveldt/plugin.audio.spotify) - A Kodi plugin for Spotify.
- [raspotify](https://github.com/dtcooper/raspotify) - A Spotify Connect client that mostly Just Works™
- [Spotifyd](https://github.com/Spotifyd/spotifyd) - A stripped down librespot UNIX daemon.
- [rpi-audio-receiver](https://github.com/nicokaiser/rpi-audio-receiver) - easy Raspbian install scripts for Spotifyd, Bluetooth, Shairport and other audio receivers
- [Spotcontrol](https://github.com/badfortrains/spotcontrol) - A golang implementation of a Spotify Connect controller. No Playback functionality.
- [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot.
- [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client.
- [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot.
- [Spot](https://github.com/xou816/spot) - Gtk/Rust native Spotify client for the GNOME desktop.
- [Snapcast](https://github.com/badaix/snapcast) - synchronised multi-room audio player that uses librespot as its source for Spotify content
- [MuPiBox](https://mupibox.de/) - Portable music box for Spotify and local media based on Raspberry Pi. Operated via touchscreen. Suitable for children and older people.
- [RoPieee](https://ropieee.org) - An easy-to-use Raspberry Pi image for network audio streaming solutions.
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
We will support the latest release and main development branch with security updates.
## Reporting a Vulnerability
If you believe to have found a vulnerability in `librespot` itself or as a result from
one of its dependencies, please report it by contacting one or more of the active
maintainers directly, allowing no less than three calendar days to receive a response.
If you believe that the vulnerability is public knowledge or already being exploited
in the wild, regardless of having received a response to your direct messages or not,
please create an issue report to warn other users about continued use and instruct
them on any known workarounds.
On your report you may expect feedback on whether we believe that the vulnerability
is indeed applicable and if so, when and how it may be fixed. You may expect to
be asked for assistance with review and testing.
================================================
FILE: audio/Cargo.toml
================================================
[package]
name = "librespot-audio"
version = "0.8.0"
rust-version.workspace = true
authors = ["Paul Lietar <paul@lietar.net>"]
license.workspace = true
description = "The audio fetching logic for librespot"
repository.workspace = true
edition.workspace = true
[features]
# Refer to the workspace Cargo.toml for the list of features
default = ["native-tls"]
# TLS backend propagation
native-tls = ["librespot-core/native-tls"]
rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"]
rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"]
[dependencies]
librespot-core = { version = "0.8.0", path = "../core", default-features = false }
aes = "0.8"
bytes = "1"
ctr = "0.9"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
http-body-util = "0.1"
hyper = { version = "1.6", features = ["http1", "http2"] }
hyper-util = { version = "0.1", features = ["client", "http2"] }
log = "0.4"
tempfile = "3"
thiserror = "2"
tokio = { version = "1", features = ["macros", "sync"] }
[lints]
workspace = true
================================================
FILE: audio/src/decrypt.rs
================================================
use std::io;
use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
use librespot_core::audio_key::AudioKey;
const AUDIO_AESIV: [u8; 16] = [
0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93,
];
pub struct AudioDecrypt<T: io::Read> {
// a `None` cipher is a convenience to make `AudioDecrypt` pass files unaltered
cipher: Option<Aes128Ctr>,
reader: T,
}
impl<T: io::Read> AudioDecrypt<T> {
pub fn new(key: Option<AudioKey>, reader: T) -> AudioDecrypt<T> {
let cipher = if let Some(key) = key {
Aes128Ctr::new_from_slices(&key.0, &AUDIO_AESIV).ok()
} else {
// some files are unencrypted
None
};
AudioDecrypt { cipher, reader }
}
}
impl<T: io::Read> io::Read for AudioDecrypt<T> {
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
let len = self.reader.read(output)?;
if let Some(ref mut cipher) = self.cipher {
cipher.apply_keystream(&mut output[..len]);
}
Ok(len)
}
}
impl<T: io::Read + io::Seek> io::Seek for AudioDecrypt<T> {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
let newpos = self.reader.seek(pos)?;
if let Some(ref mut cipher) = self.cipher {
cipher.seek(newpos);
}
Ok(newpos)
}
}
================================================
FILE: audio/src/fetch/mod.rs
================================================
mod receive;
use std::{
cmp::min,
fs,
io::{self, Read, Seek, SeekFrom},
sync::{
Arc, OnceLock,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
sync::{Condvar, Mutex},
time::Duration,
};
use futures_util::{StreamExt, TryFutureExt, future::IntoStream};
use hyper::{Response, StatusCode, body::Incoming, header::CONTENT_RANGE};
use hyper_util::client::legacy::ResponseFuture;
use tempfile::NamedTempFile;
use thiserror::Error;
use tokio::sync::{Semaphore, mpsc, oneshot};
use librespot_core::{Error, FileId, Session, cdn_url::CdnUrl};
use self::receive::audio_file_fetch;
use crate::range_set::{Range, RangeSet};
pub type AudioFileResult = Result<(), librespot_core::Error>;
const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned";
#[derive(Error, Debug)]
pub enum AudioFileError {
#[error("other end of channel disconnected")]
Channel,
#[error("required header not found")]
Header,
#[error("streamer received no data")]
NoData,
#[error("no output available")]
Output,
#[error("invalid status code {0}")]
StatusCode(StatusCode),
#[error("wait timeout exceeded")]
WaitTimeout,
}
impl From<AudioFileError> for Error {
fn from(err: AudioFileError) -> Self {
match err {
AudioFileError::Channel => Error::aborted(err),
AudioFileError::Header => Error::unavailable(err),
AudioFileError::NoData => Error::unavailable(err),
AudioFileError::Output => Error::aborted(err),
AudioFileError::StatusCode(_) => Error::failed_precondition(err),
AudioFileError::WaitTimeout => Error::deadline_exceeded(err),
}
}
}
#[derive(Clone)]
pub struct AudioFetchParams {
/// The minimum size of a block that is requested from the Spotify servers in one request.
/// This is the block size that is typically requested while doing a `seek()` on a file.
/// The Symphonia decoder requires this to be a power of 2 and > 32 kB.
/// Note: smaller requests can happen if part of the block is downloaded already.
pub minimum_download_size: usize,
/// The minimum network throughput that we expect. Together with the minimum download size,
/// this will determine the time we will wait for a response.
pub minimum_throughput: usize,
/// The ping time that is used for calculations before a ping time was actually measured.
pub initial_ping_time_estimate: Duration,
/// If the measured ping time to the Spotify server is larger than this value, it is capped
/// to avoid run-away block sizes and pre-fetching.
pub maximum_assumed_ping_time: Duration,
/// Before playback starts, this many seconds of data must be present.
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
/// of audio data may be larger or smaller.
pub read_ahead_before_playback: Duration,
/// While playing back, this many seconds of data ahead of the current read position are
/// requested.
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
/// of audio data may be larger or smaller.
pub read_ahead_during_playback: Duration,
/// If the amount of data that is pending (requested but not received) is less than a certain amount,
/// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more
/// data is calculated as `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>`
pub prefetch_threshold_factor: f32,
/// The time we will wait to obtain status updates on downloading.
pub download_timeout: Duration,
}
impl Default for AudioFetchParams {
fn default() -> Self {
let minimum_download_size = 64 * 1024;
let minimum_throughput = 8 * 1024;
Self {
minimum_download_size,
minimum_throughput,
initial_ping_time_estimate: Duration::from_millis(500),
maximum_assumed_ping_time: Duration::from_millis(1500),
read_ahead_before_playback: Duration::from_secs(1),
read_ahead_during_playback: Duration::from_secs(5),
prefetch_threshold_factor: 4.0,
download_timeout: Duration::from_secs(
(minimum_download_size / minimum_throughput) as u64,
),
}
}
}
static AUDIO_FETCH_PARAMS: OnceLock<AudioFetchParams> = OnceLock::new();
impl AudioFetchParams {
pub fn set(params: AudioFetchParams) -> Result<(), AudioFetchParams> {
AUDIO_FETCH_PARAMS.set(params)
}
pub fn get() -> &'static AudioFetchParams {
AUDIO_FETCH_PARAMS.get_or_init(AudioFetchParams::default)
}
}
pub enum AudioFile {
Cached(fs::File),
Streaming(AudioFileStreaming),
}
#[derive(Debug)]
pub struct StreamingRequest {
streamer: IntoStream<ResponseFuture>,
initial_response: Option<Response<Incoming>>,
offset: usize,
length: usize,
}
#[derive(Debug)]
pub enum StreamLoaderCommand {
Fetch(Range), // signal the stream loader to fetch a range of the file
Close, // terminate and don't load any more data
}
#[derive(Clone)]
pub struct StreamLoaderController {
channel_tx: Option<mpsc::UnboundedSender<StreamLoaderCommand>>,
stream_shared: Option<Arc<AudioFileShared>>,
file_size: usize,
}
impl StreamLoaderController {
pub fn len(&self) -> usize {
self.file_size
}
pub fn is_empty(&self) -> bool {
self.file_size == 0
}
pub fn range_available(&self, range: Range) -> bool {
if let Some(ref shared) = self.stream_shared {
let download_status = shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
range.length
<= download_status
.downloaded
.contained_length_from_value(range.start)
} else {
range.length <= self.len() - range.start
}
}
pub fn range_to_end_available(&self) -> bool {
match self.stream_shared {
Some(ref shared) => {
let read_position = shared.read_position();
self.range_available(Range::new(read_position, self.len() - read_position))
}
None => true,
}
}
pub fn ping_time(&self) -> Option<Duration> {
self.stream_shared.as_ref().map(|shared| shared.ping_time())
}
fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
if let Some(ref channel) = self.channel_tx {
// Ignore the error in case the channel has been closed already.
// This means that the file was completely downloaded.
let _ = channel.send(command);
}
}
pub fn fetch(&self, range: Range) {
// signal the stream loader to fetch a range of the file
self.send_stream_loader_command(StreamLoaderCommand::Fetch(range));
}
pub fn fetch_blocking(&self, mut range: Range) -> AudioFileResult {
// signal the stream loader to tech a range of the file and block until it is loaded.
// ensure the range is within the file's bounds.
if range.start >= self.len() {
range.length = 0;
} else if range.end() > self.len() {
range.length = self.len() - range.start;
}
self.fetch(range);
if let Some(ref shared) = self.stream_shared {
let mut download_status = shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
let download_timeout = AudioFetchParams::get().download_timeout;
while range.length
> download_status
.downloaded
.contained_length_from_value(range.start)
{
let (new_download_status, wait_result) = shared
.cond
.wait_timeout(download_status, download_timeout)
.expect(DOWNLOAD_STATUS_POISON_MSG);
download_status = new_download_status;
if wait_result.timed_out() {
return Err(AudioFileError::WaitTimeout.into());
}
if range.length
> (download_status
.downloaded
.union(&download_status.requested)
.contained_length_from_value(range.start))
{
// For some reason, the requested range is neither downloaded nor requested.
// This could be due to a network error. Request it again.
self.fetch(range);
}
}
}
Ok(())
}
pub fn fetch_next_and_wait(
&self,
request_length: usize,
wait_length: usize,
) -> AudioFileResult {
match self.stream_shared {
Some(ref shared) => {
let start = shared.read_position();
let request_range = Range {
start,
length: request_length,
};
self.fetch(request_range);
let wait_range = Range {
start,
length: wait_length,
};
self.fetch_blocking(wait_range)
}
None => Ok(()),
}
}
pub fn set_random_access_mode(&self) {
// optimise download strategy for random access
if let Some(ref shared) = self.stream_shared {
shared.set_download_streaming(false)
}
}
pub fn set_stream_mode(&self) {
// optimise download strategy for streaming
if let Some(ref shared) = self.stream_shared {
shared.set_download_streaming(true)
}
}
pub fn close(&self) {
// terminate stream loading and don't load any more data for this file.
self.send_stream_loader_command(StreamLoaderCommand::Close);
}
pub fn from_local_file(file_size: u64) -> Self {
Self {
channel_tx: None,
stream_shared: None,
file_size: file_size as usize,
}
}
}
pub struct AudioFileStreaming {
read_file: fs::File,
position: u64,
stream_loader_command_tx: mpsc::UnboundedSender<StreamLoaderCommand>,
shared: Arc<AudioFileShared>,
}
struct AudioFileDownloadStatus {
requested: RangeSet,
downloaded: RangeSet,
}
struct AudioFileShared {
cdn_url: String,
file_size: usize,
bytes_per_second: usize,
cond: Condvar,
download_status: Mutex<AudioFileDownloadStatus>,
download_streaming: AtomicBool,
download_slots: Semaphore,
ping_time_ms: AtomicUsize,
read_position: AtomicUsize,
throughput: AtomicUsize,
}
impl AudioFileShared {
fn is_download_streaming(&self) -> bool {
self.download_streaming.load(Ordering::Acquire)
}
fn set_download_streaming(&self, streaming: bool) {
self.download_streaming.store(streaming, Ordering::Release)
}
fn ping_time(&self) -> Duration {
let ping_time_ms = self.ping_time_ms.load(Ordering::Acquire);
if ping_time_ms > 0 {
Duration::from_millis(ping_time_ms as u64)
} else {
AudioFetchParams::get().initial_ping_time_estimate
}
}
fn set_ping_time(&self, duration: Duration) {
self.ping_time_ms
.store(duration.as_millis() as usize, Ordering::Release)
}
fn throughput(&self) -> usize {
self.throughput.load(Ordering::Acquire)
}
fn set_throughput(&self, throughput: usize) {
self.throughput.store(throughput, Ordering::Release)
}
fn read_position(&self) -> usize {
self.read_position.load(Ordering::Acquire)
}
fn set_read_position(&self, position: u64) {
self.read_position
.store(position as usize, Ordering::Release)
}
}
impl AudioFile {
pub async fn open(
session: &Session,
file_id: FileId,
bytes_per_second: usize,
) -> Result<AudioFile, Error> {
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
debug!("File {file_id} already in cache");
return Ok(AudioFile::Cached(file));
}
debug!("Downloading file {file_id}");
let (complete_tx, complete_rx) = oneshot::channel();
let streaming =
AudioFileStreaming::open(session.clone(), file_id, complete_tx, bytes_per_second);
let session_ = session.clone();
session.spawn(complete_rx.map_ok(move |mut file| {
debug!("Downloading file {file_id} complete");
if let Some(cache) = session_.cache() {
if let Some(cache_id) = cache.file_path(file_id) {
if let Err(e) = cache.save_file(file_id, &mut file) {
error!("Error caching file {file_id} to {cache_id:?}: {e}");
} else {
debug!("File {file_id} cached to {cache_id:?}");
}
}
}
}));
Ok(AudioFile::Streaming(streaming.await?))
}
pub fn get_stream_loader_controller(&self) -> Result<StreamLoaderController, Error> {
let controller = match self {
AudioFile::Streaming(stream) => StreamLoaderController {
channel_tx: Some(stream.stream_loader_command_tx.clone()),
stream_shared: Some(stream.shared.clone()),
file_size: stream.shared.file_size,
},
AudioFile::Cached(file) => StreamLoaderController {
channel_tx: None,
stream_shared: None,
file_size: file.metadata()?.len() as usize,
},
};
Ok(controller)
}
pub fn is_cached(&self) -> bool {
matches!(self, AudioFile::Cached { .. })
}
}
impl AudioFileStreaming {
pub async fn open(
session: Session,
file_id: FileId,
complete_tx: oneshot::Sender<NamedTempFile>,
bytes_per_second: usize,
) -> Result<AudioFileStreaming, Error> {
let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?;
let minimum_download_size = AudioFetchParams::get().minimum_download_size;
let mut response_streamer_url = None;
let urls = cdn_url.try_get_urls()?;
for url in &urls {
// When the audio file is really small, this `download_size` may turn out to be
// larger than the audio file we're going to stream later on. This is OK; requesting
// `Content-Range` > `Content-Length` will return the complete file with status code
// 206 Partial Content.
let mut streamer =
session
.spclient()
.stream_from_cdn(*url, 0, minimum_download_size)?;
// Get the first chunk with the headers to get the file size.
// The remainder of that chunk with possibly also a response body is then
// further processed in `audio_file_fetch`.
let streamer_result = tokio::time::timeout(Duration::from_secs(10), streamer.next())
.await
.map_err(|_| AudioFileError::WaitTimeout.into())
.and_then(|x| x.ok_or_else(|| AudioFileError::NoData.into()))
.and_then(|x| x.map_err(Error::from));
match streamer_result {
Ok(r) => {
response_streamer_url = Some((r, streamer, url));
break;
}
Err(e) => warn!("Fetching {url} failed with error {e:?}, trying next"),
}
}
let Some((response, streamer, url)) = response_streamer_url else {
return Err(Error::unavailable(format!(
"{} URLs failed, none left to try",
urls.len()
)));
};
trace!("Streaming from {url}");
let code = response.status();
if code != StatusCode::PARTIAL_CONTENT {
debug!("Opening audio file expected partial content but got: {code}");
return Err(AudioFileError::StatusCode(code).into());
}
let header_value = response
.headers()
.get(CONTENT_RANGE)
.ok_or(AudioFileError::Header)?;
let str_value = header_value.to_str()?;
let hyphen_index = str_value.find('-').unwrap_or_default();
let slash_index = str_value.find('/').unwrap_or_default();
let upper_bound: usize = str_value[hyphen_index + 1..slash_index].parse()?;
let file_size = str_value[slash_index + 1..].parse()?;
let initial_request = StreamingRequest {
streamer,
initial_response: Some(response),
offset: 0,
length: upper_bound + 1,
};
let shared = Arc::new(AudioFileShared {
cdn_url: url.to_string(),
file_size,
bytes_per_second,
cond: Condvar::new(),
download_status: Mutex::new(AudioFileDownloadStatus {
requested: RangeSet::new(),
downloaded: RangeSet::new(),
}),
download_streaming: AtomicBool::new(false),
download_slots: Semaphore::new(1),
ping_time_ms: AtomicUsize::new(0),
read_position: AtomicUsize::new(0),
throughput: AtomicUsize::new(0),
});
let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone())?;
write_file.as_file().set_len(file_size as u64)?;
let read_file = write_file.reopen()?;
let (stream_loader_command_tx, stream_loader_command_rx) =
mpsc::unbounded_channel::<StreamLoaderCommand>();
session.spawn(audio_file_fetch(
session.clone(),
shared.clone(),
initial_request,
write_file,
stream_loader_command_rx,
complete_tx,
));
Ok(AudioFileStreaming {
read_file,
position: 0,
stream_loader_command_tx,
shared,
})
}
}
impl Read for AudioFileStreaming {
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
let offset = self.position as usize;
if offset >= self.shared.file_size {
return Ok(0);
}
let length = min(output.len(), self.shared.file_size - offset);
if length == 0 {
return Ok(0);
}
let read_ahead_during_playback = AudioFetchParams::get().read_ahead_during_playback;
let length_to_request = if self.shared.is_download_streaming() {
let length_to_request = length
+ (read_ahead_during_playback.as_secs_f32() * self.shared.bytes_per_second as f32)
as usize;
// Due to the read-ahead stuff, we potentially request more than the actual request demanded.
min(length_to_request, self.shared.file_size - offset)
} else {
length
};
let mut ranges_to_request = RangeSet::new();
ranges_to_request.add_range(&Range::new(offset, length_to_request));
let mut download_status = self
.shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
ranges_to_request.subtract_range_set(&download_status.downloaded);
ranges_to_request.subtract_range_set(&download_status.requested);
for &range in ranges_to_request.iter() {
self.stream_loader_command_tx
.send(StreamLoaderCommand::Fetch(range))
.map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?;
}
let download_timeout = AudioFetchParams::get().download_timeout;
while !download_status.downloaded.contains(offset) {
let (new_download_status, wait_result) = self
.shared
.cond
.wait_timeout(download_status, download_timeout)
.expect(DOWNLOAD_STATUS_POISON_MSG);
download_status = new_download_status;
if wait_result.timed_out() {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
Error::deadline_exceeded(AudioFileError::WaitTimeout),
));
}
}
let available_length = download_status
.downloaded
.contained_length_from_value(offset);
drop(download_status);
self.position = self.read_file.seek(SeekFrom::Start(offset as u64))?;
let read_len = min(length, available_length);
let read_len = self.read_file.read(&mut output[..read_len])?;
self.position += read_len as u64;
self.shared.set_read_position(self.position);
Ok(read_len)
}
}
impl Seek for AudioFileStreaming {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
// If we are already at this position, we don't need to switch download mode.
// These checks and locks are less expensive than interrupting streaming.
let current_position = self.position as i64;
let requested_pos = match pos {
SeekFrom::Start(pos) => pos as i64,
SeekFrom::End(pos) => self.shared.file_size as i64 - pos - 1,
SeekFrom::Current(pos) => current_position + pos,
};
if requested_pos == current_position {
return Ok(current_position as u64);
}
// Again if we have already downloaded this part.
let available = self
.shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG)
.downloaded
.contains(requested_pos as usize);
let mut was_streaming = false;
if !available {
// Ensure random access mode if we need to download this part.
// Checking whether we are streaming now is a micro-optimization
// to save an atomic load.
was_streaming = self.shared.is_download_streaming();
if was_streaming {
self.shared.set_download_streaming(false);
}
}
self.position = self.read_file.seek(pos)?;
self.shared.set_read_position(self.position);
if !available && was_streaming {
self.shared.set_download_streaming(true);
}
Ok(self.position)
}
}
impl Read for AudioFile {
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
match *self {
AudioFile::Cached(ref mut file) => file.read(output),
AudioFile::Streaming(ref mut file) => file.read(output),
}
}
}
impl Seek for AudioFile {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match *self {
AudioFile::Cached(ref mut file) => file.seek(pos),
AudioFile::Streaming(ref mut file) => file.seek(pos),
}
}
}
================================================
FILE: audio/src/fetch/receive.rs
================================================
use std::{
cmp::{max, min},
io::{Seek, SeekFrom, Write},
sync::Arc,
time::{Duration, Instant},
};
use bytes::Bytes;
use futures_util::StreamExt;
use http_body_util::BodyExt;
use hyper::StatusCode;
use tempfile::NamedTempFile;
use tokio::sync::{mpsc, oneshot};
use librespot_core::{Error, http_client::HttpClient, session::Session};
use crate::range_set::{Range, RangeSet};
use super::{
AudioFetchParams, AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand,
StreamingRequest,
};
struct PartialFileData {
offset: usize,
data: Bytes,
}
enum ReceivedData {
Throughput(usize),
ResponseTime(Duration),
Data(PartialFileData),
}
const ONE_SECOND: Duration = Duration::from_secs(1);
const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned";
async fn receive_data(
shared: Arc<AudioFileShared>,
file_data_tx: mpsc::UnboundedSender<ReceivedData>,
mut request: StreamingRequest,
) -> AudioFileResult {
let mut offset = request.offset;
let mut actual_length = 0;
let permit = shared.download_slots.acquire().await?;
let request_time = Instant::now();
let mut measure_ping_time = true;
let mut measure_throughput = true;
let result: Result<_, Error> = loop {
let response = match request.initial_response.take() {
Some(data) => {
// the request was already made outside of this function
measure_ping_time = false;
measure_throughput = false;
data
}
None => match request.streamer.next().await {
Some(Ok(response)) => response,
Some(Err(e)) => break Err(e.into()),
None => {
if actual_length != request.length {
let msg = format!("did not expect body to contain {actual_length} bytes");
break Err(Error::data_loss(msg));
}
break Ok(());
}
},
};
if measure_ping_time {
let duration = Instant::now().duration_since(request_time);
// may be zero if we are handling an initial response
if duration.as_millis() > 0 {
file_data_tx.send(ReceivedData::ResponseTime(duration))?;
measure_ping_time = false;
}
}
let code = response.status();
if code != StatusCode::PARTIAL_CONTENT {
if code == StatusCode::TOO_MANY_REQUESTS {
if let Some(duration) = HttpClient::get_retry_after(response.headers()) {
warn!(
"Rate limiting, retrying in {} seconds...",
duration.as_secs()
);
// sleeping here means we hold onto this streamer "slot"
// (we don't decrease the number of open requests)
tokio::time::sleep(duration).await;
}
}
break Err(AudioFileError::StatusCode(code).into());
}
let body = response.into_body();
let data = match body
.collect()
.await
.map(http_body_util::Collected::to_bytes)
{
Ok(bytes) => bytes,
Err(e) => break Err(e.into()),
};
let data_size = data.len();
file_data_tx.send(ReceivedData::Data(PartialFileData { offset, data }))?;
actual_length += data_size;
offset += data_size;
};
drop(request.streamer);
if measure_throughput {
let duration = Instant::now().duration_since(request_time).as_millis();
if actual_length > 0 && duration > 0 {
let throughput = ONE_SECOND.as_millis() * actual_length as u128 / duration;
file_data_tx.send(ReceivedData::Throughput(throughput as usize))?;
}
}
let bytes_remaining = request.length - actual_length;
if bytes_remaining > 0 {
{
let missing_range = Range::new(offset, bytes_remaining);
let mut download_status = shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
download_status.requested.subtract_range(&missing_range);
shared.cond.notify_all();
}
}
drop(permit);
if let Err(e) = result {
error!(
"Streamer error requesting range {} +{}: {:?}",
request.offset, request.length, e
);
return Err(e);
}
Ok(())
}
struct AudioFileFetch {
session: Session,
shared: Arc<AudioFileShared>,
output: Option<NamedTempFile>,
file_data_tx: mpsc::UnboundedSender<ReceivedData>,
complete_tx: Option<oneshot::Sender<NamedTempFile>>,
network_response_times: Vec<Duration>,
params: AudioFetchParams,
}
// Might be replaced by enum from std once stable
#[derive(PartialEq, Eq)]
enum ControlFlow {
Break,
Continue,
}
impl AudioFileFetch {
fn has_download_slots_available(&self) -> bool {
self.shared.download_slots.available_permits() > 0
}
fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult {
if length < self.params.minimum_download_size {
length = self.params.minimum_download_size;
}
// If we are in streaming mode (so not seeking) then start downloading as large
// of chunks as possible for better throughput and improved CPU usage, while
// still being reasonably responsive (~1 second) in case we want to seek.
if self.shared.is_download_streaming() {
let throughput = self.shared.throughput();
length = max(length, throughput);
}
if offset + length > self.shared.file_size {
length = self.shared.file_size - offset;
}
let mut ranges_to_request = RangeSet::new();
ranges_to_request.add_range(&Range::new(offset, length));
// The iteration that follows spawns streamers fast, without awaiting them,
// so holding the lock for the entire scope of this function should be faster
// then locking and unlocking multiple times.
let mut download_status = self
.shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
ranges_to_request.subtract_range_set(&download_status.downloaded);
ranges_to_request.subtract_range_set(&download_status.requested);
// TODO : refresh cdn_url when the token expired
for range in ranges_to_request.iter() {
let streamer = self.session.spclient().stream_from_cdn(
&self.shared.cdn_url,
range.start,
range.length,
)?;
download_status.requested.add_range(range);
let streaming_request = StreamingRequest {
streamer,
initial_response: None,
offset: range.start,
length: range.length,
};
self.session.spawn(receive_data(
self.shared.clone(),
self.file_data_tx.clone(),
streaming_request,
));
}
Ok(())
}
fn pre_fetch_more_data(&mut self, bytes: usize) -> AudioFileResult {
// determine what is still missing
let mut missing_data = RangeSet::new();
missing_data.add_range(&Range::new(0, self.shared.file_size));
{
let download_status = self
.shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
missing_data.subtract_range_set(&download_status.downloaded);
missing_data.subtract_range_set(&download_status.requested);
}
// download data from after the current read position first
let mut tail_end = RangeSet::new();
let read_position = self.shared.read_position();
tail_end.add_range(&Range::new(
read_position,
self.shared.file_size - read_position,
));
let tail_end = tail_end.intersection(&missing_data);
if !tail_end.is_empty() {
let range = tail_end.get_range(0);
let offset = range.start;
let length = min(range.length, bytes);
self.download_range(offset, length)?;
} else if !missing_data.is_empty() {
// ok, the tail is downloaded, download something fom the beginning.
let range = missing_data.get_range(0);
let offset = range.start;
let length = min(range.length, bytes);
self.download_range(offset, length)?;
}
Ok(())
}
fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFlow, Error> {
match data {
ReceivedData::Throughput(mut throughput) => {
if throughput < self.params.minimum_throughput {
warn!(
"Throughput {} kbps lower than minimum {}, setting to minimum",
throughput / 1000,
self.params.minimum_throughput / 1000,
);
throughput = self.params.minimum_throughput;
}
let old_throughput = self.shared.throughput();
let avg_throughput = if old_throughput > 0 {
(old_throughput + throughput) / 2
} else {
throughput
};
// print when the new estimate deviates by more than 10% from the last
if f32::abs((avg_throughput as f32 - old_throughput as f32) / old_throughput as f32)
> 0.1
{
trace!(
"Throughput now estimated as: {} kbps",
avg_throughput / 1000
);
}
self.shared.set_throughput(avg_throughput);
}
ReceivedData::ResponseTime(mut response_time) => {
if response_time > self.params.maximum_assumed_ping_time {
warn!(
"Time to first byte {} ms exceeds maximum {}, setting to maximum",
response_time.as_millis(),
self.params.maximum_assumed_ping_time.as_millis()
);
response_time = self.params.maximum_assumed_ping_time;
}
let old_ping_time_ms = self.shared.ping_time().as_millis();
// prune old response times. Keep at most two so we can push a third.
while self.network_response_times.len() >= 3 {
self.network_response_times.remove(0);
}
// record the response time
self.network_response_times.push(response_time);
// stats::median is experimental. So we calculate the median of up to three ourselves.
let ping_time = {
match self.network_response_times.len() {
1 => self.network_response_times[0],
2 => (self.network_response_times[0] + self.network_response_times[1]) / 2,
3 => {
let mut times = self.network_response_times.clone();
times.sort_unstable();
times[1]
}
_ => unreachable!(),
}
};
// print when the new estimate deviates by more than 10% from the last
if f32::abs(
(ping_time.as_millis() as f32 - old_ping_time_ms as f32)
/ old_ping_time_ms as f32,
) > 0.1
{
trace!(
"Time to first byte now estimated as: {} ms",
ping_time.as_millis()
);
}
// store our new estimate for everyone to see
self.shared.set_ping_time(ping_time);
}
ReceivedData::Data(data) => {
match self.output.as_mut() {
Some(output) => {
output.seek(SeekFrom::Start(data.offset as u64))?;
output.write_all(data.data.as_ref())?;
}
None => return Err(AudioFileError::Output.into()),
}
let received_range = Range::new(data.offset, data.data.len());
let full = {
let mut download_status = self
.shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
download_status.downloaded.add_range(&received_range);
self.shared.cond.notify_all();
download_status.downloaded.contained_length_from_value(0)
>= self.shared.file_size
};
if full {
self.finish()?;
return Ok(ControlFlow::Break);
}
}
}
Ok(ControlFlow::Continue)
}
fn handle_stream_loader_command(
&mut self,
cmd: StreamLoaderCommand,
) -> Result<ControlFlow, Error> {
match cmd {
StreamLoaderCommand::Fetch(request) => {
self.download_range(request.start, request.length)?
}
StreamLoaderCommand::Close => return Ok(ControlFlow::Break),
}
Ok(ControlFlow::Continue)
}
fn finish(&mut self) -> AudioFileResult {
let output = self.output.take();
let complete_tx = self.complete_tx.take();
if let Some(mut output) = output {
output.rewind()?;
if let Some(complete_tx) = complete_tx {
complete_tx
.send(output)
.map_err(|_| AudioFileError::Channel)?;
}
}
Ok(())
}
}
pub(super) async fn audio_file_fetch(
session: Session,
shared: Arc<AudioFileShared>,
initial_request: StreamingRequest,
output: NamedTempFile,
mut stream_loader_command_rx: mpsc::UnboundedReceiver<StreamLoaderCommand>,
complete_tx: oneshot::Sender<NamedTempFile>,
) -> AudioFileResult {
let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel();
{
let requested_range = Range::new(
initial_request.offset,
initial_request.offset + initial_request.length,
);
let mut download_status = shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
download_status.requested.add_range(&requested_range);
}
session.spawn(receive_data(
shared.clone(),
file_data_tx.clone(),
initial_request,
));
let params = AudioFetchParams::get();
let mut fetch = AudioFileFetch {
session: session.clone(),
shared,
output: Some(output),
file_data_tx,
complete_tx: Some(complete_tx),
network_response_times: Vec::with_capacity(3),
params: params.clone(),
};
loop {
tokio::select! {
cmd = stream_loader_command_rx.recv() => {
match cmd {
Some(cmd) => {
if fetch.handle_stream_loader_command(cmd)? == ControlFlow::Break {
break;
}
}
None => break,
}
}
data = file_data_rx.recv() => {
match data {
Some(data) => {
if fetch.handle_file_data(data)? == ControlFlow::Break {
break;
}
}
None => break,
}
},
else => (),
}
if fetch.shared.is_download_streaming() && fetch.has_download_slots_available() {
let bytes_pending: usize = {
let download_status = fetch
.shared
.download_status
.lock()
.expect(DOWNLOAD_STATUS_POISON_MSG);
download_status
.requested
.minus(&download_status.downloaded)
.len()
};
let ping_time_seconds = fetch.shared.ping_time().as_secs_f32();
let throughput = fetch.shared.throughput();
let desired_pending_bytes = max(
(params.prefetch_threshold_factor
* ping_time_seconds
* fetch.shared.bytes_per_second as f32) as usize,
(ping_time_seconds * throughput as f32) as usize,
);
if bytes_pending < desired_pending_bytes {
fetch.pre_fetch_more_data(desired_pending_bytes - bytes_pending)?;
}
}
}
Ok(())
}
================================================
FILE: audio/src/lib.rs
================================================
#[macro_use]
extern crate log;
mod decrypt;
mod fetch;
mod range_set;
pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFetchParams, AudioFile, AudioFileError, StreamLoaderController};
================================================
FILE: audio/src/range_set.rs
================================================
use std::{
cmp::{max, min},
fmt,
slice::Iter,
};
#[derive(Copy, Clone, Debug)]
pub struct Range {
pub start: usize,
pub length: usize,
}
impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}, {}]", self.start, self.start + self.length - 1)
}
}
impl Range {
pub fn new(start: usize, length: usize) -> Range {
Range { start, length }
}
pub fn end(&self) -> usize {
self.start + self.length
}
}
#[derive(Debug, Clone)]
pub struct RangeSet {
ranges: Vec<Range>,
}
impl fmt::Display for RangeSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "(")?;
for range in self.ranges.iter() {
write!(f, "{range}")?;
}
write!(f, ")")
}
}
impl RangeSet {
pub fn new() -> RangeSet {
RangeSet {
ranges: Vec::<Range>::new(),
}
}
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
pub fn len(&self) -> usize {
self.ranges.iter().map(|r| r.length).sum()
}
pub fn get_range(&self, index: usize) -> Range {
self.ranges[index]
}
pub fn iter(&self) -> Iter<'_, Range> {
self.ranges.iter()
}
pub fn contains(&self, value: usize) -> bool {
for range in self.ranges.iter() {
if value < range.start {
return false;
} else if range.start <= value && value < range.end() {
return true;
}
}
false
}
pub fn contained_length_from_value(&self, value: usize) -> usize {
for range in self.ranges.iter() {
if value < range.start {
return 0;
} else if range.start <= value && value < range.end() {
return range.end() - value;
}
}
0
}
#[allow(dead_code)]
pub fn contains_range_set(&self, other: &RangeSet) -> bool {
for range in other.ranges.iter() {
if self.contained_length_from_value(range.start) < range.length {
return false;
}
}
true
}
pub fn add_range(&mut self, range: &Range) {
if range.length == 0 {
// the interval is empty -> nothing to do.
return;
}
for index in 0..self.ranges.len() {
// the new range is clear of any ranges we already iterated over.
if range.end() < self.ranges[index].start {
// the new range starts after anything we already passed and ends before the next range starts (they don't touch) -> insert it.
self.ranges.insert(index, *range);
return;
} else if range.start <= self.ranges[index].end()
&& self.ranges[index].start <= range.end()
{
// the new range overlaps (or touches) the first range. They are to be merged.
// In addition we might have to merge further ranges in as well.
let mut new_range = *range;
while index < self.ranges.len() && self.ranges[index].start <= new_range.end() {
let new_end = max(new_range.end(), self.ranges[index].end());
new_range.start = min(new_range.start, self.ranges[index].start);
new_range.length = new_end - new_range.start;
self.ranges.remove(index);
}
self.ranges.insert(index, new_range);
return;
}
}
// the new range is after everything else -> just add it
self.ranges.push(*range);
}
#[allow(dead_code)]
pub fn add_range_set(&mut self, other: &RangeSet) {
for range in other.ranges.iter() {
self.add_range(range);
}
}
#[allow(dead_code)]
pub fn union(&self, other: &RangeSet) -> RangeSet {
let mut result = self.clone();
result.add_range_set(other);
result
}
pub fn subtract_range(&mut self, range: &Range) {
if range.length == 0 {
return;
}
for index in 0..self.ranges.len() {
// the ranges we already passed don't overlap with the range to remove
if range.end() <= self.ranges[index].start {
// the remaining ranges are past the one to subtract. -> we're done.
return;
} else if range.start <= self.ranges[index].start
&& self.ranges[index].start < range.end()
{
// the range to subtract started before the current range and reaches into the current range
// -> we have to remove the beginning of the range or the entire range and do the same for following ranges.
while index < self.ranges.len() && self.ranges[index].end() <= range.end() {
self.ranges.remove(index);
}
if index < self.ranges.len() && self.ranges[index].start < range.end() {
self.ranges[index].length -= range.end() - self.ranges[index].start;
self.ranges[index].start = range.end();
}
return;
} else if range.end() < self.ranges[index].end() {
// the range to subtract punches a hole into the current range -> we need to create two smaller ranges.
let first_range = Range {
start: self.ranges[index].start,
length: range.start - self.ranges[index].start,
};
self.ranges[index].length -= range.end() - self.ranges[index].start;
self.ranges[index].start = range.end();
self.ranges.insert(index, first_range);
return;
} else if range.start < self.ranges[index].end() {
// the range truncates the existing range -> truncate the range. Let the for loop take care of overlaps with other ranges.
self.ranges[index].length = range.start - self.ranges[index].start;
}
}
}
pub fn subtract_range_set(&mut self, other: &RangeSet) {
for range in other.ranges.iter() {
self.subtract_range(range);
}
}
pub fn minus(&self, other: &RangeSet) -> RangeSet {
let mut result = self.clone();
result.subtract_range_set(other);
result
}
pub fn intersection(&self, other: &RangeSet) -> RangeSet {
let mut result = RangeSet::new();
let mut self_index: usize = 0;
let mut other_index: usize = 0;
while self_index < self.ranges.len() && other_index < other.ranges.len() {
if self.ranges[self_index].end() <= other.ranges[other_index].start {
// skip the interval
self_index += 1;
} else if other.ranges[other_index].end() <= self.ranges[self_index].start {
// skip the interval
other_index += 1;
} else {
// the two intervals overlap. Add the union and advance the index of the one that ends first.
let new_start = max(
self.ranges[self_index].start,
other.ranges[other_index].start,
);
let new_end = min(
self.ranges[self_index].end(),
other.ranges[other_index].end(),
);
result.add_range(&Range::new(new_start, new_end - new_start));
if self.ranges[self_index].end() <= other.ranges[other_index].end() {
self_index += 1;
} else {
other_index += 1;
}
}
}
result
}
}
================================================
FILE: cache/.gitignore
================================================
*
!.gitignore
================================================
FILE: connect/Cargo.toml
================================================
[package]
name = "librespot-connect"
version = "0.8.0"
rust-version.workspace = true
authors = ["Paul Lietar <paul@lietar.net>"]
license.workspace = true
description = "The Spotify Connect logic for librespot"
repository.workspace = true
edition.workspace = true
[features]
# Refer to the workspace Cargo.toml for the list of features
default = ["native-tls"]
# TLS backend propagation
native-tls = ["librespot-core/native-tls"]
rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"]
rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"]
[dependencies]
librespot-core = { version = "0.8.0", path = "../core", default-features = false }
librespot-playback = { version = "0.8.0", path = "../playback", default-features = false }
librespot-protocol = { version = "0.8.0", path = "../protocol", default-features = false }
futures-util = { version = "0.3", default-features = false, features = ["std"] }
log = "0.4"
protobuf = "3.7"
rand = { version = "0.9", default-features = false, features = ["small_rng"] }
serde_json = "1.0"
thiserror = "2"
tokio = { version = "1", features = ["macros", "sync"] }
tokio-stream = { version = "0.1", default-features = false }
uuid = { version = "1.18", default-features = false, features = ["v4"] }
[lints]
workspace = true
================================================
FILE: connect/README.md
================================================
[//]: # (This readme is optimized for inline rustdoc, if some links don't work, they will when included in lib.rs)
# Connect
The connect module of librespot. Provides the option to create your own connect device
and stream to it like any other official spotify client.
The [`Spirc`] is the entrypoint to creating your own connect device. It can be
configured with the given [`ConnectConfig`] options and requires some additional data
to start up the device.
When creating a new [`Spirc`] it returns two items. The [`Spirc`] itself, which is can
be used as to control the local connect device. And a [`Future`](std::future::Future),
lets name it `SpircTask`, that starts and executes the event loop of the connect device
when awaited.
A basic example in which the `Spirc` and `SpircTask` is used can be found here:
[`examples/play_connect.rs`](../examples/play_connect.rs).
# Example
```rust
use std::{future::Future, thread};
use librespot_connect::{ConnectConfig, Spirc};
use librespot_core::{authentication::Credentials, Error, Session, SessionConfig};
use librespot_playback::{
audio_backend, mixer,
config::{AudioFormat, PlayerConfig},
mixer::{MixerConfig, NoOpVolume},
player::Player
};
async fn create_basic_spirc() -> Result<(), Error> {
let credentials = Credentials::with_access_token("access-token-here");
let session = Session::new(SessionConfig::default(), None);
let backend = audio_backend::find(None).expect("will default to rodio");
let player = Player::new(
PlayerConfig::default(),
session.clone(),
Box::new(NoOpVolume),
move || {
let format = AudioFormat::default();
let device = None;
backend(device, format)
},
);
let mixer = mixer::find(None).expect("will default to SoftMixer");
let (spirc, spirc_task): (Spirc, _) = Spirc::new(
ConnectConfig::default(),
session,
credentials,
player,
mixer(MixerConfig::default())?
).await?;
Ok(())
}
```
================================================
FILE: connect/src/context_resolver.rs
================================================
use crate::{
core::{Error, Session},
protocol::{
autoplay_context_request::AutoplayContextRequest, context::Context,
transfer_state::TransferState,
},
state::{ConnectState, context::ContextType},
};
use std::{
cmp::PartialEq,
collections::{HashMap, VecDeque},
fmt::{Display, Formatter},
hash::Hash,
time::Duration,
};
use thiserror::Error as ThisError;
use tokio::time::Instant;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
enum Resolve {
Uri(String),
Context(Context),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(super) enum ContextAction {
Append,
Replace,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(super) struct ResolveContext {
resolve: Resolve,
fallback: Option<String>,
update: ContextType,
action: ContextAction,
}
impl ResolveContext {
fn append_context(uri: impl Into<String>) -> Self {
Self {
resolve: Resolve::Uri(uri.into()),
fallback: None,
update: ContextType::Default,
action: ContextAction::Append,
}
}
pub fn from_uri(
uri: impl Into<String>,
fallback: impl Into<String>,
update: ContextType,
action: ContextAction,
) -> Self {
let fallback_uri = fallback.into();
Self {
resolve: Resolve::Uri(uri.into()),
fallback: (!fallback_uri.is_empty()).then_some(fallback_uri),
update,
action,
}
}
pub fn from_context(context: Context, update: ContextType, action: ContextAction) -> Self {
Self {
resolve: Resolve::Context(context),
fallback: None,
update,
action,
}
}
/// the uri which should be used to resolve the context, might not be the context uri
fn resolve_uri(&self) -> Option<&str> {
// it's important to call this always, or at least for every ResolveContext
// otherwise we might not even check if we need to fallback and just use the fallback uri
match self.resolve {
Resolve::Uri(ref uri) => ConnectState::valid_resolve_uri(uri),
Resolve::Context(ref ctx) => {
ConnectState::find_valid_uri(ctx.uri.as_deref(), ctx.pages.first())
}
}
.or(self.fallback.as_deref())
}
/// the actual context uri
fn context_uri(&self) -> &str {
match self.resolve {
Resolve::Uri(ref uri) => uri,
Resolve::Context(ref ctx) => ctx.uri.as_deref().unwrap_or_default(),
}
}
}
impl Display for ResolveContext {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"resolve_uri: <{:?}>, context_uri: <{}>, update: <{:?}>",
self.resolve_uri(),
self.context_uri(),
self.update,
)
}
}
#[derive(Debug, ThisError)]
enum ContextResolverError {
#[error("no next context to resolve")]
NoNext,
#[error("tried appending context with {0} pages")]
UnexpectedPagesSize(usize),
#[error("tried resolving not allowed context: {0:?}")]
NotAllowedContext(String),
}
impl From<ContextResolverError> for Error {
fn from(value: ContextResolverError) -> Self {
Error::failed_precondition(value)
}
}
pub struct ContextResolver {
session: Session,
queue: VecDeque<ResolveContext>,
unavailable_contexts: HashMap<ResolveContext, Instant>,
}
// time after which an unavailable context is retried
const RETRY_UNAVAILABLE: Duration = Duration::from_secs(3600);
impl ContextResolver {
pub fn new(session: Session) -> Self {
Self {
session,
queue: VecDeque::new(),
unavailable_contexts: HashMap::new(),
}
}
pub fn add(&mut self, resolve: ResolveContext) {
let last_try = self
.unavailable_contexts
.get(&resolve)
.map(Instant::elapsed);
let last_try = if matches!(last_try, Some(last_try) if last_try > RETRY_UNAVAILABLE) {
let _ = self.unavailable_contexts.remove(&resolve);
debug!(
"context was requested {}s ago, trying again to resolve the requested context",
last_try.expect("checked by condition").as_secs()
);
None
} else {
last_try
};
if last_try.is_some() {
debug!("tried loading unavailable context: {resolve}");
return;
} else if self.queue.contains(&resolve) {
debug!("update for {resolve} is already added");
return;
} else {
trace!(
"added {} to resolver queue",
resolve.resolve_uri().unwrap_or(resolve.context_uri())
)
}
self.queue.push_back(resolve)
}
pub fn add_list(&mut self, resolve: Vec<ResolveContext>) {
for resolve in resolve {
self.add(resolve)
}
}
pub fn remove_used_and_invalid(&mut self) {
if let Some((_, _, remove)) = self.find_next() {
let _ = self.queue.drain(0..remove); // remove invalid
}
self.queue.pop_front(); // remove used
}
pub fn clear(&mut self) {
self.queue = VecDeque::new()
}
fn find_next(&self) -> Option<(&ResolveContext, &str, usize)> {
for idx in 0..self.queue.len() {
let next = self.queue.get(idx)?;
match next.resolve_uri() {
None => {
warn!("skipped {idx} because of invalid resolve_uri: {next}");
continue;
}
Some(uri) => return Some((next, uri, idx)),
}
}
None
}
pub fn has_next(&self) -> bool {
self.find_next().is_some()
}
pub async fn get_next_context(
&self,
recent_track_uri: impl Fn() -> Vec<String>,
) -> Result<Context, Error> {
let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?;
match next.update {
ContextType::Default => {
let mut ctx = self.session.spclient().get_context(resolve_uri).await;
if let Ok(ctx) = ctx.as_mut() {
ctx.uri = Some(next.context_uri().to_string());
ctx.url = ctx.uri.as_ref().map(|s| format!("context://{s}"));
}
ctx
}
ContextType::Autoplay => {
if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:")
{
// autoplay is not supported for podcasts
Err(ContextResolverError::NotAllowedContext(
resolve_uri.to_string(),
))?
}
let request = AutoplayContextRequest {
context_uri: Some(resolve_uri.to_string()),
recent_track_uri: recent_track_uri(),
..Default::default()
};
self.session.spclient().get_autoplay_context(&request).await
}
}
}
pub fn mark_next_unavailable(&mut self) {
if let Some((next, _, _)) = self.find_next() {
self.unavailable_contexts
.insert(next.clone(), Instant::now());
}
}
pub fn apply_next_context(
&self,
state: &mut ConnectState,
mut context: Context,
) -> Result<Option<Vec<ResolveContext>>, Error> {
let (next, _, _) = self.find_next().ok_or(ContextResolverError::NoNext)?;
let remaining = match next.action {
ContextAction::Append if context.pages.len() == 1 => state
.fill_context_from_page(context.pages.remove(0))
.map(|_| None),
ContextAction::Replace => {
let remaining = state.update_context(context, next.update);
if let Resolve::Context(ref ctx) = next.resolve {
state.merge_context(ctx.pages.clone().pop());
}
remaining
}
ContextAction::Append => {
warn!("unexpected page size: {context:#?}");
Err(ContextResolverError::UnexpectedPagesSize(context.pages.len()).into())
}
}?;
Ok(remaining.map(|remaining| {
remaining
.into_iter()
.map(ResolveContext::append_context)
.collect::<Vec<_>>()
}))
}
pub fn try_finish(
&self,
state: &mut ConnectState,
transfer_state: &mut Option<TransferState>,
) -> bool {
let (next, _, _) = match self.find_next() {
None => return false,
Some(next) => next,
};
// when there is only one update type, we are the last of our kind, so we should update the state
if self
.queue
.iter()
.filter(|resolve| resolve.update == next.update)
.count()
!= 1
{
return false;
}
match (next.update, state.active_context) {
(ContextType::Default, ContextType::Default) | (ContextType::Autoplay, _) => {
debug!(
"last item of type <{:?}>, finishing state setup",
next.update
);
}
(ContextType::Default, _) => {
debug!("skipped finishing default, because it isn't the active context");
return false;
}
}
let active_ctx = state.get_context(state.active_context);
let res = if let Some(transfer_state) = transfer_state.take() {
state.finish_transfer(transfer_state)
} else if state.shuffling_context() && next.update == ContextType::Default {
state.shuffle_new()
} else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) {
// has context, and context is not touched
// when the index is not zero, the next index was already evaluated elsewhere
let ctx = active_ctx.expect("checked by precondition");
let idx = ConnectState::find_index_in_context(ctx, |t| {
state.current_track(|c| t.uri == c.uri)
})
.ok();
state.reset_playback_to_position(idx)
} else {
state.fill_up_next_tracks()
};
if let Err(why) = res {
error!("setup of state failed: {why}, last used resolve {next:#?}")
}
state.update_restrictions();
state.update_queue_revision();
true
}
}
================================================
FILE: connect/src/lib.rs
================================================
#![warn(missing_docs)]
#![doc=include_str!("../README.md")]
#[macro_use]
extern crate log;
use librespot_core as core;
use librespot_playback as playback;
use librespot_protocol as protocol;
mod context_resolver;
mod model;
mod shuffle_vec;
mod spirc;
mod state;
pub use model::*;
pub use spirc::*;
pub use state::*;
================================================
FILE: connect/src/model.rs
================================================
use crate::{
core::dealer::protocol::SkipTo, protocol::context_player_options::ContextPlayerOptionOverrides,
};
use std::ops::Deref;
/// Request for loading playback
#[derive(Debug, Clone)]
pub struct LoadRequest {
pub(super) context: PlayContext,
pub(super) options: LoadRequestOptions,
}
impl Deref for LoadRequest {
type Target = LoadRequestOptions;
fn deref(&self) -> &Self::Target {
&self.options
}
}
#[derive(Debug, Clone)]
pub(super) enum PlayContext {
Uri(String),
Tracks(Vec<String>),
}
/// The parameters for creating a load request
#[derive(Debug, Default, Clone)]
pub struct LoadRequestOptions {
/// Whether the given tracks should immediately start playing, or just be initially loaded.
pub start_playing: bool,
/// Start the playback at a specific point of the track.
///
/// The provided value is used as milliseconds. Providing a value greater
/// than the track duration will start the track at the beginning.
pub seek_to: u32,
/// Options that decide how the context starts playing
pub context_options: Option<LoadContextOptions>,
/// Decides the starting position in the given context.
///
/// If the provided item doesn't exist or is out of range,
/// the playback starts at the beginning of the context.
///
/// If `None` is provided and `shuffle` is `true`, a random track is played, otherwise the first
pub playing_track: Option<PlayingTrack>,
}
/// The options which decide how the playback is started
///
/// Separated into an `enum` to exclude the other variants from being used
/// simultaneously, as they are not compatible.
#[derive(Debug, Clone)]
pub enum LoadContextOptions {
/// Starts the context with options
Options(Options),
/// Starts the playback as the autoplay variant of the context
///
/// This is the same as finishing a context and
/// automatically continuing playback of similar tracks
Autoplay,
}
/// The available options that indicate how to start the context
#[derive(Debug, Default, Clone)]
pub struct Options {
/// Start the context in shuffle mode
pub shuffle: bool,
/// Start the context in repeat mode
pub repeat: bool,
/// Start the context, repeating the first track until skipped or manually disabled
pub repeat_track: bool,
}
impl From<ContextPlayerOptionOverrides> for Options {
fn from(value: ContextPlayerOptionOverrides) -> Self {
Self {
shuffle: value.shuffling_context.unwrap_or_default(),
repeat: value.repeating_context.unwrap_or_default(),
repeat_track: value.repeating_track.unwrap_or_default(),
}
}
}
impl LoadRequest {
/// Create a load request from a `context_uri`
///
/// For supported `context_uri` see [`SpClient::get_context`](librespot_core::spclient::SpClient::get_context)
///
/// Equivalent to using [`/me/player/play`](https://developer.spotify.com/documentation/web-api/reference/start-a-users-playback)
/// and providing `context_uri`
pub fn from_context_uri(context_uri: String, options: LoadRequestOptions) -> Self {
Self {
context: PlayContext::Uri(context_uri),
options,
}
}
/// Create a load request from a set of `tracks`
///
/// Equivalent to using [`/me/player/play`](https://developer.spotify.com/documentation/web-api/reference/start-a-users-playback)
/// and providing `uris`
pub fn from_tracks(tracks: Vec<String>, options: LoadRequestOptions) -> Self {
Self {
context: PlayContext::Tracks(tracks),
options,
}
}
}
/// An item that represent a track to play
#[derive(Debug, Clone)]
pub enum PlayingTrack {
/// Represent the track at a given index.
Index(u32),
/// Represent the uri of a track.
Uri(String),
#[doc(hidden)]
/// Represent an internal identifier from spotify.
///
/// The internal identifier is not the id contained in the uri. And rather
/// an unrelated id probably unique in spotify's internal database. But that's
/// just speculation.
///
/// This identifier is not available by any public api. It's used for varies in
/// any spotify client, like sorting, displaying which track is currently played
/// and skipping to a track. Mobile uses it pretty intensively but also web and
/// desktop seem to make use of it.
Uid(String),
}
impl TryFrom<SkipTo> for PlayingTrack {
type Error = ();
fn try_from(value: SkipTo) -> Result<Self, Self::Error> {
// order of checks is important, as the index can be 0, but still has an uid or uri provided,
// so we only use the index as last resort
if let Some(uri) = value.track_uri {
Ok(PlayingTrack::Uri(uri))
} else if let Some(uid) = value.track_uid {
Ok(PlayingTrack::Uid(uid))
} else if let Some(index) = value.track_index {
Ok(PlayingTrack::Index(index))
} else {
Err(())
}
}
}
#[derive(Debug)]
pub(super) enum SpircPlayStatus {
Stopped,
LoadingPlay {
position_ms: u32,
},
LoadingPause {
position_ms: u32,
},
Playing {
nominal_start_time: i64,
preloading_of_next_track_triggered: bool,
},
Paused {
position_ms: u32,
preloading_of_next_track_triggered: bool,
},
}
================================================
FILE: connect/src/shuffle_vec.rs
================================================
use rand::{Rng, SeedableRng, rngs::SmallRng};
use std::{
ops::{Deref, DerefMut},
vec::IntoIter,
};
#[derive(Debug, Clone, Default)]
pub struct ShuffleVec<T> {
vec: Vec<T>,
indices: Option<Vec<usize>>,
/// This is primarily necessary to ensure that shuffle does not behave out of place.
///
/// For that reason we swap the first track with the currently playing track. By that we ensure
/// that the shuffle state is consistent between resets of the state because the first track is
/// always the track with which we started playing when switching to shuffle.
original_first_position: Option<usize>,
}
impl<T: PartialEq> PartialEq for ShuffleVec<T> {
fn eq(&self, other: &Self) -> bool {
self.vec == other.vec
}
}
impl<T> Deref for ShuffleVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.vec
}
}
impl<T> DerefMut for ShuffleVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.vec.as_mut()
}
}
impl<T> IntoIterator for ShuffleVec<T> {
type Item = T;
type IntoIter = IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.vec.into_iter()
}
}
impl<T> From<Vec<T>> for ShuffleVec<T> {
fn from(vec: Vec<T>) -> Self {
Self {
vec,
original_first_position: None,
indices: None,
}
}
}
impl<T> ShuffleVec<T> {
pub fn shuffle_with_seed<F: Fn(&T) -> bool>(&mut self, seed: u64, is_first: F) {
self.shuffle_with_rng(SmallRng::seed_from_u64(seed), is_first)
}
pub fn shuffle_with_rng<F: Fn(&T) -> bool>(&mut self, mut rng: impl Rng, is_first: F) {
if self.vec.len() <= 1 {
info!("skipped shuffling for less or equal one item");
return;
}
if self.indices.is_some() {
self.unshuffle()
}
let indices: Vec<_> = {
(1..self.vec.len())
.rev()
.map(|i| rng.random_range(0..i + 1))
.collect()
};
for (i, &rnd_ind) in (1..self.vec.len()).rev().zip(&indices) {
self.vec.swap(i, rnd_ind);
}
self.indices = Some(indices);
self.original_first_position = self.vec.iter().position(is_first);
if let Some(first_pos) = self.original_first_position {
self.vec.swap(0, first_pos)
}
}
pub fn unshuffle(&mut self) {
let indices = match self.indices.take() {
Some(indices) => indices,
None => return,
};
if let Some(first_pos) = self.original_first_position {
self.vec.swap(0, first_pos);
self.original_first_position = None;
}
for i in 1..self.vec.len() {
match indices.get(self.vec.len() - i - 1) {
None => return,
Some(n) => self.vec.swap(*n, i),
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use rand::Rng;
use std::ops::Range;
fn base(range: Range<usize>) -> (ShuffleVec<usize>, u64) {
let seed = rand::rng().random_range(0..10_000_000_000_000);
let vec = range.collect::<Vec<_>>();
(vec.into(), seed)
}
#[test]
fn test_shuffle_without_first() {
let (base_vec, seed) = base(0..100);
let mut shuffled_vec = base_vec.clone();
shuffled_vec.shuffle_with_seed(seed, |_| false);
let mut different_shuffled_vec = base_vec.clone();
different_shuffled_vec.shuffle_with_seed(seed, |_| false);
assert_eq!(
shuffled_vec, different_shuffled_vec,
"shuffling with the same seed has the same result"
);
let mut unshuffled_vec = shuffled_vec.clone();
unshuffled_vec.unshuffle();
assert_eq!(
base_vec, unshuffled_vec,
"unshuffle restores the original state"
);
}
#[test]
fn test_shuffle_with_first() {
const MAX_RANGE: usize = 200;
let (base_vec, seed) = base(0..MAX_RANGE);
let rand_first = rand::rng().random_range(0..MAX_RANGE);
let mut shuffled_with_first = base_vec.clone();
shuffled_with_first.shuffle_with_seed(seed, |i| i == &rand_first);
assert_eq!(
Some(&rand_first),
shuffled_with_first.first(),
"after shuffling the first is expected to be the given item"
);
let mut shuffled_without_first = base_vec.clone();
shuffled_without_first.shuffle_with_seed(seed, |_| false);
let mut switched_positions = Vec::with_capacity(2);
for (i, without_first_value) in shuffled_without_first.iter().enumerate() {
if without_first_value != &shuffled_with_first[i] {
switched_positions.push(i);
} else {
assert_eq!(
without_first_value, &shuffled_with_first[i],
"shuffling with the same seed has the same result"
);
}
}
assert_eq!(
switched_positions.len(),
2,
"only the switched positions should be different"
);
assert_eq!(
shuffled_with_first[switched_positions[0]],
shuffled_without_first[switched_positions[1]],
"the switched values should be equal"
);
assert_eq!(
shuffled_with_first[switched_positions[1]],
shuffled_without_first[switched_positions[0]],
"the switched values should be equal"
)
}
}
================================================
FILE: connect/src/spirc.rs
================================================
use crate::{
LoadContextOptions, LoadRequestOptions, PlayContext,
context_resolver::{ContextAction, ContextResolver, ResolveContext},
core::{
Error, Session, SpotifyUri,
authentication::Credentials,
dealer::{
manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply},
protocol::{Command, FallbackWrapper, Message, Request},
},
session::UserAttributes,
spclient::TransferRequest,
},
model::{LoadRequest, PlayingTrack, SpircPlayStatus},
playback::{
mixer::Mixer,
player::{Player, PlayerEvent, PlayerEventChannel, QueueTrack},
},
protocol::{
connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand},
context::Context,
explicit_content_pubsub::UserAttributesUpdate,
player::ProvidedTrack,
playlist4_external::PlaylistModificationInfo,
social_connect_v2::SessionUpdate,
transfer_state::TransferState,
user_attributes::UserAttributesMutation,
},
state::{
context::{ContextType, ResetContext},
provider::IsProvider,
{ConnectConfig, ConnectState},
},
};
use futures_util::StreamExt;
use librespot_protocol::context_page::ContextPage;
use protobuf::MessageField;
use std::{
future::Future,
sync::Arc,
sync::atomic::{AtomicUsize, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use thiserror::Error;
use tokio::{sync::mpsc, time::sleep};
#[derive(Debug, Error)]
enum SpircError {
#[error("response payload empty")]
NoData,
#[error("{0} had no uri")]
NoUri(&'static str),
#[error("message pushed for another URI")]
InvalidUri(String),
#[error("failed to put connect state for new device")]
FailedDealerSetup,
#[error("unknown endpoint: {0:#?}")]
UnknownEndpoint(serde_json::Value),
}
impl From<SpircError> for Error {
fn from(err: SpircError) -> Self {
use SpircError::*;
match err {
NoData | NoUri(_) => Error::unavailable(err),
InvalidUri(_) | FailedDealerSetup => Error::aborted(err),
UnknownEndpoint(_) => Error::unimplemented(err),
}
}
}
struct SpircTask {
player: Arc<Player>,
mixer: Arc<dyn Mixer>,
/// the state management object
connect_state: ConnectState,
connect_established: bool,
play_request_id: Option<u64>,
play_status: SpircPlayStatus,
connection_id_update: BoxedStreamResult<String>,
connect_state_update: BoxedStreamResult<ClusterUpdate>,
connect_state_volume_update: BoxedStreamResult<SetVolumeCommand>,
connect_state_logout_request: BoxedStreamResult<LogoutCommand>,
playlist_update: BoxedStreamResult<PlaylistModificationInfo>,
session_update: BoxedStreamResult<FallbackWrapper<SessionUpdate>>,
connect_state_command: BoxedStream<RequestReply>,
user_attributes_update: BoxedStreamResult<UserAttributesUpdate>,
user_attributes_mutation: BoxedStreamResult<UserAttributesMutation>,
commands: Option<mpsc::UnboundedReceiver<SpircCommand>>,
player_events: Option<PlayerEventChannel>,
context_resolver: ContextResolver,
emit_set_queue_events: bool,
shutdown: bool,
session: Session,
/// is set when transferring, and used after resolving the contexts to finish the transfer
pub transfer_state: Option<TransferState>,
/// when set to true, it will update the volume after [VOLUME_UPDATE_DELAY],
/// when no other future resolves, otherwise resets the delay
update_volume: bool,
/// when set to true, it will update the volume after [UPDATE_STATE_DELAY],
/// when no other future resolves, otherwise resets the delay
update_state: bool,
spirc_id: usize,
}
static SPIRC_COUNTER: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug)]
enum SpircCommand {
Play,
PlayPause,
Pause,
Prev,
Next,
VolumeUp,
VolumeDown,
Shutdown,
Shuffle(bool),
Repeat(bool),
RepeatTrack(bool),
Disconnect { pause: bool },
SetPosition(u32),
SetVolume(u16),
Activate,
Transfer(Option<TransferRequest>),
Load(LoadRequest),
AddToQueue(SpotifyUri),
}
const CONTEXT_FETCH_THRESHOLD: usize = 2;
// delay to update volume after a certain amount of time, instead on each update request
const VOLUME_UPDATE_DELAY: Duration = Duration::from_millis(500);
// to reduce updates to remote, we group some request by waiting for a set amount of time
const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200);
/// The spotify connect handle
pub struct Spirc {
commands: mpsc::UnboundedSender<SpircCommand>,
}
impl Spirc {
/// Initializes a new spotify connect device
///
/// The returned tuple consists out of a handle to the [`Spirc`] that
/// can control the local connect device when active. And a [`Future`]
/// which represents the [`Spirc`] event loop that processes the whole
/// connect device logic.
pub async fn new(
config: ConnectConfig,
session: Session,
credentials: Credentials,
player: Arc<Player>,
mixer: Arc<dyn Mixer>,
) -> Result<(Spirc, impl Future<Output = ()>), Error> {
fn extract_connection_id(msg: Message) -> Result<String, Error> {
let connection_id = msg
.headers
.get("Spotify-Connection-Id")
.ok_or_else(|| SpircError::InvalidUri(msg.uri.clone()))?;
Ok(connection_id.to_owned())
}
let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel);
debug!("new Spirc[{spirc_id}]");
let emit_set_queue_events = config.emit_set_queue_events;
let connect_state = ConnectState::new(config, &session);
let connection_id_update = session
.dealer()
.listen_for("hm://pusher/v1/connections/", extract_connection_id)?;
let connect_state_update = session
.dealer()
.listen_for("hm://connect-state/v1/cluster", Message::from_raw)?;
let connect_state_volume_update = session
.dealer()
.listen_for("hm://connect-state/v1/connect/volume", Message::from_raw)?;
let connect_state_logout_request = session
.dealer()
.listen_for("hm://connect-state/v1/connect/logout", Message::from_raw)?;
let playlist_update = session
.dealer()
.listen_for("hm://playlist/v2/playlist/", Message::from_raw)?;
let session_update = session
.dealer()
.listen_for("social-connect/v2/session_update", Message::try_from_json)?;
let user_attributes_update = session
.dealer()
.listen_for("spotify:user:attributes:update", Message::from_raw)?;
// can be trigger by toggling autoplay in a desktop client
let user_attributes_mutation = session
.dealer()
.listen_for("spotify:user:attributes:mutated", Message::from_raw)?;
let connect_state_command = session
.dealer()
.handle_for("hm://connect-state/v1/player/command")?;
// pre-acquire client_token, preventing multiple request while running
let _ = session.spclient().client_token().await?;
// Connect *after* all message listeners are registered
session.connect(credentials, true).await?;
// pre-acquire access_token (we need to be authenticated to retrieve a token)
let _ = session.login5().auth_token().await?;
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let player_events = player.get_player_event_channel();
let mut task = SpircTask {
player,
mixer,
connect_state,
connect_established: false,
play_request_id: None,
play_status: SpircPlayStatus::Stopped,
connection_id_update,
connect_state_update,
connect_state_volume_update,
connect_state_logout_request,
playlist_update,
session_update,
connect_state_command,
user_attributes_update,
user_attributes_mutation,
commands: Some(cmd_rx),
player_events: Some(player_events),
context_resolver: ContextResolver::new(session.clone()),
emit_set_queue_events,
shutdown: false,
session,
transfer_state: None,
update_volume: false,
update_state: false,
spirc_id,
};
let spirc = Spirc { commands: cmd_tx };
let initial_volume = task.connect_state.device_info().volume;
task.connect_state.set_volume(0);
match initial_volume.try_into() {
Ok(volume) => {
task.set_volume(volume);
// we don't want to update the volume initially,
// we just want to set the mixer to the correct volume
task.update_volume = false;
}
Err(why) => error!("failed to update initial volume: {why}"),
};
Ok((spirc, task.run()))
}
/// Safely shutdowns the spirc.
///
/// This pauses the playback, disconnects the connect device and
/// bring the future initially returned to an end.
pub fn shutdown(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Shutdown)?)
}
/// Resumes the playback
///
/// Does nothing if we are not the active device, or it isn't paused.
pub fn play(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Play)?)
}
/// Resumes or pauses the playback
///
/// Does nothing if we are not the active device.
pub fn play_pause(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::PlayPause)?)
}
/// Pauses the playback
///
/// Does nothing if we are not the active device, or if it isn't playing.
pub fn pause(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Pause)?)
}
/// Seeks to the beginning or skips to the previous track.
///
/// Seeks to the beginning when the current track position
/// is greater than 3 seconds.
///
/// Does nothing if we are not the active device.
pub fn prev(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Prev)?)
}
/// Skips to the next track.
///
/// Does nothing if we are not the active device.
pub fn next(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Next)?)
}
/// Increases the volume by configured steps of [ConnectConfig].
///
/// Does nothing if we are not the active device.
pub fn volume_up(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::VolumeUp)?)
}
/// Decreases the volume by configured steps of [ConnectConfig].
///
/// Does nothing if we are not the active device.
pub fn volume_down(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::VolumeDown)?)
}
/// Shuffles the playback according to the value.
///
/// If true shuffles/reshuffles the playback. Otherwise, does
/// nothing (if not shuffled) or unshuffles the playback while
/// resuming at the position of the current track.
///
/// Does nothing if we are not the active device.
pub fn shuffle(&self, shuffle: bool) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Shuffle(shuffle))?)
}
/// Repeats the playback context according to the value.
///
/// Does nothing if we are not the active device.
pub fn repeat(&self, repeat: bool) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Repeat(repeat))?)
}
/// Repeats the current track if true.
///
/// Does nothing if we are not the active device.
///
/// Skipping to the next track disables the repeating.
pub fn repeat_track(&self, repeat: bool) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::RepeatTrack(repeat))?)
}
/// Update the volume to the given value.
///
/// Does nothing if we are not the active device.
pub fn set_volume(&self, volume: u16) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::SetVolume(volume))?)
}
/// Updates the position to the given value.
///
/// Does nothing if we are not the active device.
///
/// If value is greater than the track duration,
/// the update is ignored.
pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::SetPosition(position_ms))?)
}
/// Load a new context and replace the current.
///
/// Does nothing if we are not the active device.
///
/// Does not overwrite the queue.
pub fn load(&self, command: LoadRequest) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Load(command))?)
}
/// Adds a track, episode, album or playlist to the queue.
///
/// Does nothing if we are not the active device.
///
/// For albums and playlists, all tracks/episodes are resolved and added to the queue.
pub fn add_to_queue(&self, uri: SpotifyUri) -> Result<(), Error> {
if !matches!(
uri,
SpotifyUri::Track { .. }
| SpotifyUri::Episode { .. }
| SpotifyUri::Album { .. }
| SpotifyUri::Playlist { .. }
) {
return Err(Error::invalid_argument("uri"));
}
Ok(self.commands.send(SpircCommand::AddToQueue(uri))?)
}
/// Disconnects the current device and pauses the playback according the value.
///
/// Does nothing if we are not the active device.
pub fn disconnect(&self, pause: bool) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Disconnect { pause })?)
}
/// Acquires the control as active connect device.
///
/// Does not [Spirc::transfer] the playback. Does nothing if we are not the active device.
pub fn activate(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Activate)?)
}
/// Acquires the control as active connect device over the transfer flow.
///
/// Does nothing if we are not the active device.
pub fn transfer(&self, transfer_request: Option<TransferRequest>) -> Result<(), Error> {
Ok(self
.commands
.send(SpircCommand::Transfer(transfer_request))?)
}
}
impl SpircTask {
async fn run(mut self) {
// simplify unwrapping of received item or parsed result
macro_rules! unwrap {
( $next:expr, |$some:ident| $use_some:expr ) => {
match $next {
Some($some) => $use_some,
None => {
error!("{} selected, but none received", stringify!($next));
break;
}
}
};
( $next:expr, match |$ok:ident| $use_ok:expr ) => {
unwrap!($next, |$ok| match $ok {
Ok($ok) => $use_ok,
Err(why) => error!("could not parse {}: {}", stringify!($ok), why),
})
};
}
if let Err(why) = self.session.dealer().start().await {
error!("starting dealer failed: {why}");
return;
}
while !self.session.is_invalid() && !self.shutdown {
let commands = self.commands.as_mut();
let player_events = self.player_events.as_mut();
// when state and volume update have a higher priority than context resolving
// because of that the context resolving has to wait, so that the other tasks can finish
let allow_context_resolving = !self.update_state && !self.update_volume;
tokio::select! {
// startup of the dealer requires a connection_id, which is retrieved at the very beginning
connection_id_update = self.connection_id_update.next() => unwrap! {
connection_id_update,
match |connection_id| if let Err(why) = self.handle_connection_id_update(connection_id).await {
error!("failed handling connection id update: {why}");
break;
}
},
// main dealer update of any remote device updates
cluster_update = self.connect_state_update.next() => unwrap! {
cluster_update,
match |cluster_update| if let Err(e) = self.handle_cluster_update(cluster_update).await {
error!("could not dispatch connect state update: {e}");
}
},
// main dealer request handling (dealer expects an answer)
request = self.connect_state_command.next() => unwrap! {
request,
|request| if let Err(e) = self.handle_connect_state_request(request).await {
error!("couldn't handle connect state command: {e}");
}
},
// volume request handling is send separately (it's more like a fire forget)
volume_update = self.connect_state_volume_update.next() => unwrap! {
volume_update,
match |volume_update| match volume_update.volume.try_into() {
Ok(volume) => self.set_volume(volume),
Err(why) => error!("can't update volume, failed to parse i32 to u16: {why}")
}
},
logout_request = self.connect_state_logout_request.next() => unwrap! {
logout_request,
|logout_request| {
error!("received logout request, currently not supported: {logout_request:#?}");
// todo: call logout handling
}
},
playlist_update = self.playlist_update.next() => unwrap! {
playlist_update,
match |playlist_update| if let Err(why) = self.handle_playlist_modification(playlist_update) {
error!("failed to handle playlist modification: {why}")
}
},
user_attributes_update = self.user_attributes_update.next() => unwrap! {
user_attributes_update,
match |attributes| self.handle_user_attributes_update(attributes)
},
user_attributes_mutation = self.user_attributes_mutation.next() => unwrap! {
user_attributes_mutation,
match |attributes| self.handle_user_attributes_mutation(attributes)
},
session_update = self.session_update.next() => unwrap! {
session_update,
match |session_update| self.handle_session_update(session_update)
},
cmd = async { commands?.recv().await }, if commands.is_some() && self.connect_established => if let Some(cmd) = cmd {
if let Err(e) = self.handle_command(cmd).await {
debug!("could not dispatch command: {e}");
}
},
event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event {
if let Err(e) = self.handle_player_event(event) {
error!("could not dispatch player event: {e}");
}
},
_ = async { sleep(UPDATE_STATE_DELAY).await }, if self.update_state => {
self.update_state = false;
if let Err(why) = self.notify().await {
error!("state update: {why}")
}
},
_ = async { sleep(VOLUME_UPDATE_DELAY).await }, if self.update_volume => {
self.update_volume = false;
info!("delayed volume update for all devices: volume is now {}", self.connect_state.device_info().volume);
if let Err(why) = self.connect_state.notify_volume_changed(&self.session).await {
error!("error updating connect state for volume update: {why}")
}
// for some reason the web-player does need two separate updates, so that the
// position of the current track is retained, other clients also send a state
// update before they send the volume update
if let Err(why) = self.notify().await {
error!("error updating connect state for volume update: {why}")
}
},
// context resolver handling, the idea/reason behind it the following:
//
// when we request a context that has multiple pages (for example an artist)
// resolving all pages at once can take around ~1-30sec, when we resolve
// everything at once that would block our main loop for that time
//
// to circumvent this behavior, we request each context separately here and
// finish after we received our last item of a type
next_context = async {
self.context_resolver.get_next_context(|| {
// Sending local file URIs to this endpoint results in a Bad Request status.
// It's likely appropriate to filter them out anyway; Spotify's backend
// has no knowledge about these tracks and so can't do anything with them.
self.connect_state.recent_track_uris()
.into_iter()
.filter(|t| !t.starts_with("spotify:local"))
.collect::<Vec<_>>()
}).await
}, if allow_context_resolving && self.context_resolver.has_next() => {
let update_state = self.handle_next_context(next_context);
if update_state {
if let Err(why) = self.notify().await {
error!("update after context resolving failed: {why}")
}
}
},
else => break
}
}
if !self.shutdown && self.connect_state.is_active() {
warn!("unexpected shutdown");
if let Err(why) = self.handle_disconnect().await {
error!("error during disconnecting: {why}")
}
}
// this should clear the active session id, leaving an empty state
if let Err(why) = self.session.spclient().delete_connect_state_request().await {
error!("error during connect state deletion: {why}")
};
self.session.dealer().close().await;
}
fn handle_next_context(&mut self, next_context: Result<Context, Error>) -> bool {
let next_context = match next_context {
Err(why) => {
self.context_resolver.mark_next_unavailable();
self.context_resolver.remove_used_and_invalid();
error!("{why}");
return false;
}
Ok(ctx) => ctx,
};
debug!("handling next context {:?}", next_context.uri);
match self
.context_resolver
.apply_next_context(&mut self.connect_state, next_context)
{
Ok(remaining) => {
if let Some(remaining) = remaining {
self.context_resolver.add_list(remaining)
}
}
Err(why) => {
error!("{why}")
}
}
let update_state = if self
.context_resolver
.try_finish(&mut self.connect_state, &mut self.transfer_state)
{
self.add_autoplay_resolving_when_required();
true
} else {
false
};
// Fire set queue event if context was successfully loaded
if update_state {
self.emit_set_queue_event();
}
self.context_resolver.remove_used_and_invalid();
update_state
}
/// Emit set queue event via PlayerEvent
fn emit_set_queue_event(&self) {
if !self.emit_set_queue_events {
return;
}
let state_player = self.connect_state.player();
let current_track = state_player.track.as_ref().map(|t| QueueTrack {
uri: t.uri.clone(),
provider: t.provider.clone(),
});
let next_tracks: Vec<_> = state_player
.next_tracks
.iter()
.map(|t| QueueTrack {
uri: t.uri.clone(),
provider: t.provider.clone(),
})
.collect();
let prev_tracks: Vec<_> = state_player
.prev_tracks
.iter()
.map(|t| QueueTrack {
uri: t.uri.clone(),
provider: t.provider.clone(),
})
.collect();
let context_uri = self.connect_state.context_uri().clone();
self.player
.emit_set_queue_event(context_uri, current_track, next_tracks, prev_tracks);
}
// todo: is the time_delta still necessary?
fn now_ms(&self) -> i64 {
let dur = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|err| err.duration());
dur.as_millis() as i64 + 1000 * self.session.time_delta()
}
async fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> {
trace!("Received SpircCommand::{cmd:?}");
match cmd {
SpircCommand::Shutdown => {
trace!("Received SpircCommand::Shutdown");
self.handle_pause();
self.handle_disconnect().await?;
self.shutdown = true;
if let Some(rx) = self.commands.as_mut() {
rx.close()
}
}
SpircCommand::Transfer(request) if !self.connect_state.is_active() => {
let device_id = self.session.device_id();
self.session
.spclient()
.transfer(device_id, device_id, request.as_ref())
.await?;
return Ok(());
}
SpircCommand::Activate if !self.connect_state.is_active() => {
trace!("Received SpircCommand::{cmd:?}");
self.handle_activate();
return self.notify().await;
}
SpircCommand::Transfer(..) | SpircCommand::Activate => {
warn!("SpircCommand::{cmd:?} will be ignored while already active")
}
_ if !self.connect_state.is_active() => {
warn!("SpircCommand::{cmd:?} will be ignored while Not Active")
}
SpircCommand::Disconnect { pause } => {
if pause {
self.handle_pause()
}
return self.handle_disconnect().await;
}
SpircCommand::Play => self.handle_play(),
SpircCommand::PlayPause => self.handle_play_pause(),
SpircCommand::Pause => self.handle_pause(),
SpircCommand::Prev => self.handle_prev()?,
SpircCommand::Next => self.handle_next(None)?,
SpircCommand::VolumeUp => self.handle_volume_up(),
SpircCommand::VolumeDown => self.handle_volume_down(),
SpircCommand::Shuffle(shuffle) => self.handle_shuffle(shuffle)?,
SpircCommand::Repeat(repeat) => self.handle_repeat_context(repeat)?,
SpircCommand::RepeatTrack(repeat) => self.handle_repeat_track(repeat),
SpircCommand::SetPosition(position) => self.handle_seek(position),
SpircCommand::SetVolume(volume) => self.set_volume(volume),
SpircCommand::Load(command) => self.handle_load(command, None, None).await?,
SpircCommand::AddToQueue(uri) => self.handle_add_to_queue(uri).await,
};
self.notify().await
}
fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> {
if let PlayerEvent::TrackChanged { audio_item } = event {
self.connect_state.update_duration(audio_item.duration_ms);
self.update_state = true;
return Ok(());
}
// update play_request_id
if let PlayerEvent::PlayRequestIdChanged { play_request_id } = event {
self.play_request_id = Some(play_request_id);
return Ok(());
}
let is_current_track = matches! {
(event.get_play_request_id(), self.play_request_id),
(Some(event_id), Some(current_id)) if event_id == current_id
};
// we only process events if the play_request_id matches. If it doesn't, it is
// an event that belongs to a previous track and only arrives now due to a race
// condition. In this case we have updated the state already and don't want to
// mess with it.
if !is_current_track {
return Ok(());
}
match event {
PlayerEvent::EndOfTrack { .. } => {
let next_track = self
.connect_state
.repeat_track()
.then(|| self.connect_state.current_track(|t| t.uri.clone()));
self.handle_next(next_track)?
}
PlayerEvent::Loading { .. } => match self.play_status {
SpircPlayStatus::LoadingPlay { position_ms } => {
self.connect_state
.update_position(position_ms, self.now_ms());
trace!("==> LoadingPlay");
}
SpircPlayStatus::LoadingPause { position_ms } => {
self.connect_state
.update_position(position_ms, self.now_ms());
trace!("==> LoadingPause");
}
_ => {
self.connect_state.update_position(0, self.now_ms());
trace!("==> Loading");
}
},
PlayerEvent::Seeked { position_ms, .. } => {
trace!("==> Seeked");
self.connect_state
.update_position(position_ms, self.now_ms())
}
PlayerEvent::Playing { position_ms, .. }
| PlayerEvent::PositionCorrection { position_ms, .. } => {
trace!("==> Playing");
let new_nominal_start_time = self.now_ms() - position_ms as i64;
match self.play_status {
SpircPlayStatus::Playing {
ref mut nominal_start_time,
..
} => {
if (*nominal_start_time - new_nominal_start_time).abs() > 100 {
*nominal_start_time = new_nominal_start_time;
self.connect_state
.update_position(position_ms, self.now_ms());
} else {
return Ok(());
}
}
SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => {
self.connect_state
.update_position(position_ms, self.now_ms());
self.play_status = SpircPlayStatus::Playing {
nominal_start_time: new_nominal_start_time,
preloading_of_next_track_triggered: false,
};
}
_ => return Ok(()),
}
}
PlayerEvent::Paused {
position_ms: new_position_ms,
..
} => {
trace!("==> Paused");
match self.play_status {
SpircPlayStatus::Paused { .. } | SpircPlayStatus::Playing { .. } => {
self.connect_state
.update_position(new_position_ms, self.now_ms());
self.play_status = SpircPlayStatus::Paused {
position_ms: new_position_ms,
preloading_of_next_track_triggered: false,
};
}
SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => {
self.connect_state
.update_position(new_position_ms, self.now_ms());
self.play_status = SpircPlayStatus::Paused {
position_ms: new_position_ms,
preloading_of_next_track_triggered: false,
};
}
_ => return Ok(()),
}
}
PlayerEvent::Stopped { .. } => {
trace!("==> Stopped");
match self.play_status {
SpircPlayStatus::Stopped => return Ok(()),
_ => self.play_status = SpircPlayStatus::Stopped,
}
}
PlayerEvent::TimeToPreloadNextTrack { .. } => {
self.handle_preload_next_track();
return Ok(());
}
PlayerEvent::Unavailable { track_id, .. } => {
self.handle_unavailable(&track_id)?;
if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri() {
self.handle_next(None)?
}
}
_ => return Ok(()),
}
self.update_state = true;
Ok(())
}
async fn handle_connection_id_update(&mut self, connection_id: String) -> Result<(), Error> {
trace!("Received connection ID update: {connection_id:?}");
self.session.set_connection_id(&connection_id);
let cluster = match self
.connect_state
.notify_new_device_appeared(&self.session)
.await
{
Ok(res) => Cluster::parse_from_bytes(&res).ok(),
Err(why) => {
error!("{why:?}");
None
}
}
.ok_or(SpircError::FailedDealerSetup)?;
debug!(
"successfully put connect state for {} with connection-id {connection_id}",
self.session.device_id()
);
self.connect_established = true;
let same_session = cluster.player_state.session_id == self.session.session_id()
|| cluster.player_state.session_id.is_empty();
if !cluster.active_device_id.is_empty() || !same_session {
info!(
"active device is <{}> with session <{}>",
cluster.active_device_id, cluster.player_state.session_id
);
return Ok(());
} else if cluster.transfer_data.is_empty() {
debug!("got empty transfer state, do nothing");
return Ok(());
} else {
info!(
"trying to take over control automatically, session_id: {}",
cluster.player_state.session_id
)
}
use protobuf::Message;
match TransferState::parse_from_bytes(&cluster.transfer_data) {
Ok(transfer_state) => self.handle_transfer(transfer_state)?,
Err(why) => error!("failed to take over control: {why}"),
}
Ok(())
}
fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) {
trace!("Received attributes update: {update:#?}");
let attributes: UserAttributes = update
.pairs
.iter()
.map(|(key, value)| (key.to_owned(), value.to_owned()))
.collect();
self.session.set_user_attributes(attributes)
}
fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) {
for attribute in mutation.fields.iter() {
let key = &attribute.name;
if key == "autoplay" && self.session.config().autoplay.is_some() {
trace!("Autoplay override active. Ignoring mutation.");
continue;
}
if let Some(old_value) = self.session.user_data().attributes.get(key) {
let new_value = match old_value.as_ref() {
"0" => "1",
"1" => "0",
_ => old_value,
};
self.session.set_user_attribute(key, new_value);
trace!("Received attribute mutation, {key} was {old_value} is now {new_value}");
if key == "filter-explicit-content" && new_value == "1" {
self.player
.emit_filter_explicit_content_changed_event(matches!(new_value, "1"));
}
if key == "autoplay" && old_value != new_value {
self.player
.emit_auto_play_changed_event(matches!(new_value, "1"));
self.add_autoplay_resolving_when_required()
}
} else {
trace!("Received attribute mutation for {key} but key was not found!");
}
}
}
async fn handle_cluster_update(
&mut self,
mut cluster_update: ClusterUpdate,
) -> Result<(), Error> {
let reason = cluster_update.update_reason.enum_value();
let device_ids = cluster_update.devices_that_
gitextract_cwkohdkk/ ├── .devcontainer/ │ ├── Dockerfile │ ├── Dockerfile.alpine │ └── devcontainer.json ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ ├── example/ │ │ └── prepare-release.event │ ├── scripts/ │ │ └── bump-versions.sh │ └── workflows/ │ ├── build.yml │ ├── cross-compile.yml │ ├── prepare-release.yml │ ├── quality.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── COMPILING.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── PUBLISHING.md ├── README.md ├── SECURITY.md ├── audio/ │ ├── Cargo.toml │ └── src/ │ ├── decrypt.rs │ ├── fetch/ │ │ ├── mod.rs │ │ └── receive.rs │ ├── lib.rs │ └── range_set.rs ├── cache/ │ └── .gitignore ├── connect/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── context_resolver.rs │ ├── lib.rs │ ├── model.rs │ ├── shuffle_vec.rs │ ├── spirc.rs │ ├── state/ │ │ ├── context.rs │ │ ├── handle.rs │ │ ├── metadata.rs │ │ ├── options.rs │ │ ├── provider.rs │ │ ├── restrictions.rs │ │ ├── tracks.rs │ │ └── transfer.rs │ └── state.rs ├── contrib/ │ ├── Dockerfile │ ├── Dockerfile.Rpi │ ├── cross-compile-armv6hf/ │ │ ├── Dockerfile │ │ └── docker-build.sh │ ├── docker-build.sh │ ├── event_handler_example.py │ ├── librespot.service │ └── librespot.user.service ├── core/ │ ├── Cargo.toml │ ├── build.rs │ ├── src/ │ │ ├── apresolve.rs │ │ ├── audio_key.rs │ │ ├── authentication.rs │ │ ├── cache.rs │ │ ├── cdn_url.rs │ │ ├── channel.rs │ │ ├── component.rs │ │ ├── config.rs │ │ ├── connection/ │ │ │ ├── codec.rs │ │ │ ├── handshake.rs │ │ │ └── mod.rs │ │ ├── date.rs │ │ ├── dealer/ │ │ │ ├── manager.rs │ │ │ ├── maps.rs │ │ │ ├── mod.rs │ │ │ ├── protocol/ │ │ │ │ └── request.rs │ │ │ └── protocol.rs │ │ ├── deserialize_with.rs │ │ ├── diffie_hellman.rs │ │ ├── error.rs │ │ ├── file_id.rs │ │ ├── http_client.rs │ │ ├── lib.rs │ │ ├── login5.rs │ │ ├── mercury/ │ │ │ ├── mod.rs │ │ │ ├── sender.rs │ │ │ └── types.rs │ │ ├── packet.rs │ │ ├── proxytunnel.rs │ │ ├── session.rs │ │ ├── socket.rs │ │ ├── spclient.rs │ │ ├── spotify_id.rs │ │ ├── spotify_uri.rs │ │ ├── token.rs │ │ ├── util.rs │ │ └── version.rs │ └── tests/ │ └── connect.rs ├── discovery/ │ ├── Cargo.toml │ ├── examples/ │ │ ├── discovery.rs │ │ └── discovery_group.rs │ └── src/ │ ├── avahi.rs │ ├── lib.rs │ └── server.rs ├── docs/ │ ├── authentication.md │ ├── connection.md │ └── dealer.md ├── examples/ │ ├── README.md │ ├── get_token.rs │ ├── play.rs │ ├── play_connect.rs │ └── playlist_tracks.rs ├── metadata/ │ ├── Cargo.toml │ └── src/ │ ├── album.rs │ ├── artist.rs │ ├── audio/ │ │ ├── file.rs │ │ ├── item.rs │ │ └── mod.rs │ ├── availability.rs │ ├── content_rating.rs │ ├── copyright.rs │ ├── episode.rs │ ├── error.rs │ ├── external_id.rs │ ├── image.rs │ ├── lib.rs │ ├── lyrics.rs │ ├── playlist/ │ │ ├── annotation.rs │ │ ├── attribute.rs │ │ ├── diff.rs │ │ ├── item.rs │ │ ├── list.rs │ │ ├── mod.rs │ │ ├── operation.rs │ │ └── permission.rs │ ├── request.rs │ ├── restriction.rs │ ├── sale_period.rs │ ├── show.rs │ ├── track.rs │ ├── util.rs │ └── video.rs ├── oauth/ │ ├── Cargo.toml │ ├── examples/ │ │ ├── oauth_async.rs │ │ └── oauth_sync.rs │ └── src/ │ └── lib.rs ├── playback/ │ ├── Cargo.toml │ └── src/ │ ├── audio_backend/ │ │ ├── alsa.rs │ │ ├── gstreamer.rs │ │ ├── jackaudio.rs │ │ ├── mod.rs │ │ ├── pipe.rs │ │ ├── portaudio.rs │ │ ├── pulseaudio.rs │ │ ├── rodio.rs │ │ ├── sdl.rs │ │ └── subprocess.rs │ ├── config.rs │ ├── convert.rs │ ├── decoder/ │ │ ├── mod.rs │ │ ├── passthrough_decoder.rs │ │ └── symphonia_decoder.rs │ ├── dither.rs │ ├── lib.rs │ ├── local_file.rs │ ├── mixer/ │ │ ├── alsamixer.rs │ │ ├── mappings.rs │ │ ├── mod.rs │ │ └── softmixer.rs │ ├── player.rs │ └── symphonia_util.rs ├── protocol/ │ ├── Cargo.toml │ ├── build.rs │ ├── proto/ │ │ ├── AdContext.proto │ │ ├── AdDecisionEvent.proto │ │ ├── AdError.proto │ │ ├── AdEvent.proto │ │ ├── AdRequestEvent.proto │ │ ├── AdSlotEvent.proto │ │ ├── AmazonWakeUpTime.proto │ │ ├── AudioDriverError.proto │ │ ├── AudioDriverInfo.proto │ │ ├── AudioFileSelection.proto │ │ ├── AudioOffliningSettingsReport.proto │ │ ├── AudioRateLimit.proto │ │ ├── AudioSessionEvent.proto │ │ ├── AudioSettingsReport.proto │ │ ├── AudioStreamingSettingsReport.proto │ │ ├── BoomboxPlaybackInstrumentation.proto │ │ ├── BrokenObject.proto │ │ ├── CacheError.proto │ │ ├── CachePruningReport.proto │ │ ├── CacheRealmPruningReport.proto │ │ ├── CacheRealmReport.proto │ │ ├── CacheReport.proto │ │ ├── ClientLocale.proto │ │ ├── ColdStartupSequence.proto │ │ ├── CollectionLevelDbInfo.proto │ │ ├── CollectionOfflineControllerEmptyTrackList.proto │ │ ├── ConfigurationApplied.proto │ │ ├── ConfigurationFetched.proto │ │ ├── ConfigurationFetchedNonAuth.proto │ │ ├── ConnectCredentialsRequest.proto │ │ ├── ConnectDeviceDiscovered.proto │ │ ├── ConnectDialError.proto │ │ ├── ConnectMdnsPacketParseError.proto │ │ ├── ConnectPullFailure.proto │ │ ├── ConnectTransferResult.proto │ │ ├── ConnectionError.proto │ │ ├── ConnectionInfo.proto │ │ ├── ConnectionStateChange.proto │ │ ├── DefaultConfigurationApplied.proto │ │ ├── DesktopAuthenticationFailureNonAuth.proto │ │ ├── DesktopAuthenticationSuccess.proto │ │ ├── DesktopDeviceInformation.proto │ │ ├── DesktopGPUAccelerationInfo.proto │ │ ├── DesktopHighMemoryUsage.proto │ │ ├── DesktopPerformanceIssue.proto │ │ ├── DesktopUpdateDownloadComplete.proto │ │ ├── DesktopUpdateDownloadError.proto │ │ ├── DesktopUpdateMessageAction.proto │ │ ├── DesktopUpdateMessageProcessed.proto │ │ ├── DesktopUpdateResponse.proto │ │ ├── Download.proto │ │ ├── DrmRequestFailure.proto │ │ ├── EndAd.proto │ │ ├── EventSenderInternalErrorNonAuth.proto │ │ ├── EventSenderStats.proto │ │ ├── EventSenderStats2NonAuth.proto │ │ ├── ExternalDeviceInfo.proto │ │ ├── GetInfoFailures.proto │ │ ├── HeadFileDownload.proto │ │ ├── LegacyEndSong.proto │ │ ├── LocalFileSyncError.proto │ │ ├── LocalFilesError.proto │ │ ├── LocalFilesImport.proto │ │ ├── LocalFilesReport.proto │ │ ├── LocalFilesSourceReport.proto │ │ ├── MdnsLoginFailures.proto │ │ ├── MetadataExtensionClientStatistic.proto │ │ ├── Offline2ClientError.proto │ │ ├── Offline2ClientEvent.proto │ │ ├── OfflineError.proto │ │ ├── OfflineEvent.proto │ │ ├── OfflineReport.proto │ │ ├── PlaybackError.proto │ │ ├── PlaybackRetry.proto │ │ ├── PlaybackSegments.proto │ │ ├── PlayerStateRestore.proto │ │ ├── PlaylistSyncEvent.proto │ │ ├── PodcastAdSegmentReceived.proto │ │ ├── Prefetch.proto │ │ ├── PrefetchError.proto │ │ ├── ProductStateUcsVerification.proto │ │ ├── PubSubCountPerIdent.proto │ │ ├── RawCoreStream.proto │ │ ├── ReachabilityChanged.proto │ │ ├── RejectedClientEventNonAuth.proto │ │ ├── RemainingSkips.proto │ │ ├── RequestAccounting.proto │ │ ├── RequestTime.proto │ │ ├── StartTrack.proto │ │ ├── Stutter.proto │ │ ├── TierFeatureFlags.proto │ │ ├── TrackNotPlayed.proto │ │ ├── TrackStuck.proto │ │ ├── WindowSize.proto │ │ ├── apiv1.proto │ │ ├── app_state.proto │ │ ├── audio_files_extension.proto │ │ ├── audio_format.proto │ │ ├── authentication.proto │ │ ├── autodownload_backend_service.proto │ │ ├── autodownload_config_common.proto │ │ ├── autodownload_config_get_request.proto │ │ ├── autodownload_config_set_request.proto │ │ ├── automix_mode.proto │ │ ├── autoplay_context_request.proto │ │ ├── autoplay_node.proto │ │ ├── canvas.proto │ │ ├── canvas_storage.proto │ │ ├── canvaz-meta.proto │ │ ├── canvaz.proto │ │ ├── capping_data.proto │ │ ├── claas.proto │ │ ├── client-tts.proto │ │ ├── client_config.proto │ │ ├── client_update.proto │ │ ├── clips_cover.proto │ │ ├── collection/ │ │ │ ├── album_collection_state.proto │ │ │ ├── artist_collection_state.proto │ │ │ ├── episode_collection_state.proto │ │ │ ├── show_collection_state.proto │ │ │ └── track_collection_state.proto │ │ ├── collection2v2.proto │ │ ├── collection_add_remove_items_request.proto │ │ ├── collection_ban_request.proto │ │ ├── collection_decoration_policy.proto │ │ ├── collection_get_bans_request.proto │ │ ├── collection_index.proto │ │ ├── collection_item.proto │ │ ├── collection_platform_items.proto │ │ ├── collection_platform_requests.proto │ │ ├── collection_platform_responses.proto │ │ ├── concat_cosmos.proto │ │ ├── connect.proto │ │ ├── connectivity.proto │ │ ├── contains_request.proto │ │ ├── content_access_token_cosmos.proto │ │ ├── context.proto │ │ ├── context_application_desktop.proto │ │ ├── context_client_id.proto │ │ ├── context_device_desktop.proto │ │ ├── context_index.proto │ │ ├── context_installation_id.proto │ │ ├── context_monotonic_clock.proto │ │ ├── context_node.proto │ │ ├── context_page.proto │ │ ├── context_player_options.proto │ │ ├── context_processor.proto │ │ ├── context_sdk.proto │ │ ├── context_time.proto │ │ ├── context_track.proto │ │ ├── context_view.proto │ │ ├── context_view_cyclic_list.proto │ │ ├── context_view_entry.proto │ │ ├── context_view_entry_key.proto │ │ ├── cosmos_changes_request.proto │ │ ├── cosmos_decorate_request.proto │ │ ├── cosmos_get_album_list_request.proto │ │ ├── cosmos_get_artist_list_request.proto │ │ ├── cosmos_get_episode_list_request.proto │ │ ├── cosmos_get_show_list_request.proto │ │ ├── cosmos_get_tags_info_request.proto │ │ ├── cosmos_get_track_list_metadata_request.proto │ │ ├── cosmos_get_track_list_request.proto │ │ ├── cosmos_get_unplayed_episodes_request.proto │ │ ├── cuepoints.proto │ │ ├── decorate_request.proto │ │ ├── devices.proto │ │ ├── display_segments.proto │ │ ├── display_segments_extension.proto │ │ ├── entity_extension_data.proto │ │ ├── es_add_to_queue_request.proto │ │ ├── es_command_options.proto │ │ ├── es_context.proto │ │ ├── es_context_page.proto │ │ ├── es_context_player_error.proto │ │ ├── es_context_player_options.proto │ │ ├── es_context_player_state.proto │ │ ├── es_context_track.proto │ │ ├── es_delete_session.proto │ │ ├── es_get_error_request.proto │ │ ├── es_get_play_history.proto │ │ ├── es_get_position_state.proto │ │ ├── es_get_queue_request.proto │ │ ├── es_get_state_request.proto │ │ ├── es_ident.proto │ │ ├── es_ident_filter.proto │ │ ├── es_logging_params.proto │ │ ├── es_optional.proto │ │ ├── es_pause.proto │ │ ├── es_pauseresume_origin.proto │ │ ├── es_play.proto │ │ ├── es_play_options.proto │ │ ├── es_play_origin.proto │ │ ├── es_prefs.proto │ │ ├── es_prepare_play.proto │ │ ├── es_prepare_play_options.proto │ │ ├── es_provided_track.proto │ │ ├── es_pushed_message.proto │ │ ├── es_queue.proto │ │ ├── es_remote_config.proto │ │ ├── es_request_info.proto │ │ ├── es_response_with_reasons.proto │ │ ├── es_restrictions.proto │ │ ├── es_resume.proto │ │ ├── es_seek_to.proto │ │ ├── es_session_response.proto │ │ ├── es_set_options.proto │ │ ├── es_set_queue_request.proto │ │ ├── es_set_repeating_context.proto │ │ ├── es_set_repeating_track.proto │ │ ├── es_set_shuffling_context.proto │ │ ├── es_skip_next.proto │ │ ├── es_skip_prev.proto │ │ ├── es_skip_to_track.proto │ │ ├── es_stop.proto │ │ ├── es_storage.proto │ │ ├── es_update.proto │ │ ├── esperanto_options.proto │ │ ├── event_entity.proto │ │ ├── explicit_content_pubsub.proto │ │ ├── extended_metadata.proto │ │ ├── extension_descriptor_type.proto │ │ ├── extension_kind.proto │ │ ├── extracted_colors.proto │ │ ├── follow_request.proto │ │ ├── followed_users_request.proto │ │ ├── frecency.proto │ │ ├── frecency_storage.proto │ │ ├── gabito.proto │ │ ├── global_node.proto │ │ ├── google/ │ │ │ └── protobuf/ │ │ │ ├── any.proto │ │ │ ├── descriptor.proto │ │ │ ├── duration.proto │ │ │ ├── empty.proto │ │ │ ├── field_mask.proto │ │ │ ├── source_context.proto │ │ │ ├── timestamp.proto │ │ │ ├── type.proto │ │ │ └── wrappers.proto │ │ ├── greenroom_extension.proto │ │ ├── identity.proto │ │ ├── image-resolve.proto │ │ ├── installation_data.proto │ │ ├── instrumentation_params.proto │ │ ├── keyexchange.proto │ │ ├── lens-model.proto │ │ ├── lfs_secret_provider.proto │ │ ├── liked_songs_tags_sync_state.proto │ │ ├── listen_later_cosmos_response.proto │ │ ├── local_bans_storage.proto │ │ ├── local_sync_cosmos.proto │ │ ├── local_sync_state.proto │ │ ├── logging_params.proto │ │ ├── mdata.proto │ │ ├── mdata_cosmos.proto │ │ ├── mdata_storage.proto │ │ ├── media.proto │ │ ├── media_format.proto │ │ ├── media_manifest.proto │ │ ├── media_type.proto │ │ ├── media_type_node.proto │ │ ├── members_request.proto │ │ ├── members_response.proto │ │ ├── mercury.proto │ │ ├── messages/ │ │ │ └── discovery/ │ │ │ ├── force_discover.proto │ │ │ └── start_discovery.proto │ │ ├── metadata/ │ │ │ ├── album_metadata.proto │ │ │ ├── artist_metadata.proto │ │ │ ├── episode_metadata.proto │ │ │ ├── extension.proto │ │ │ ├── image_group.proto │ │ │ ├── show_metadata.proto │ │ │ └── track_metadata.proto │ │ ├── metadata.proto │ │ ├── metadata_cosmos.proto │ │ ├── metadata_esperanto.proto │ │ ├── mod.rs │ │ ├── modification_request.proto │ │ ├── net-fortune.proto │ │ ├── offline.proto │ │ ├── offline_playlists_containing.proto │ │ ├── on_demand_in_free_reason.proto │ │ ├── on_demand_set_cosmos_request.proto │ │ ├── on_demand_set_cosmos_response.proto │ │ ├── on_demand_set_response.proto │ │ ├── pause_resume_origin.proto │ │ ├── pending_event_entity.proto │ │ ├── perf_metrics_service.proto │ │ ├── pin_request.proto │ │ ├── play_history.proto │ │ ├── play_origin.proto │ │ ├── play_queue_node.proto │ │ ├── play_reason.proto │ │ ├── playback.proto │ │ ├── playback_cosmos.proto │ │ ├── playback_esperanto.proto │ │ ├── playback_platform.proto │ │ ├── playback_segments.proto │ │ ├── playback_stack.proto │ │ ├── playback_stack_v2.proto │ │ ├── playback_state.proto │ │ ├── played_state/ │ │ │ ├── episode_played_state.proto │ │ │ ├── playability_restriction.proto │ │ │ ├── show_played_state.proto │ │ │ └── track_played_state.proto │ │ ├── played_state.proto │ │ ├── playedstate.proto │ │ ├── player.proto │ │ ├── player_license.proto │ │ ├── player_model.proto │ │ ├── playlist4_external.proto │ │ ├── playlist_annotate3.proto │ │ ├── playlist_contains_request.proto │ │ ├── playlist_folder_state.proto │ │ ├── playlist_get_request.proto │ │ ├── playlist_members_request.proto │ │ ├── playlist_modification_request.proto │ │ ├── playlist_offline_request.proto │ │ ├── playlist_permission.proto │ │ ├── playlist_play_request.proto │ │ ├── playlist_playback_request.proto │ │ ├── playlist_playlist_state.proto │ │ ├── playlist_query.proto │ │ ├── playlist_request.proto │ │ ├── playlist_set_base_permission_request.proto │ │ ├── playlist_set_member_permission_request.proto │ │ ├── playlist_set_permission_request.proto │ │ ├── playlist_track_state.proto │ │ ├── playlist_user_state.proto │ │ ├── plugin.proto │ │ ├── podcast_ad_segments.proto │ │ ├── podcast_cta_cards.proto │ │ ├── podcast_paywalls_cosmos.proto │ │ ├── podcast_poll.proto │ │ ├── podcast_qna.proto │ │ ├── podcast_ratings.proto │ │ ├── podcast_segments.proto │ │ ├── podcast_segments_cosmos_request.proto │ │ ├── podcast_segments_cosmos_response.proto │ │ ├── podcast_subscription.proto │ │ ├── podcast_virality.proto │ │ ├── podcastextensions.proto │ │ ├── policy/ │ │ │ ├── album_decoration_policy.proto │ │ │ ├── artist_decoration_policy.proto │ │ │ ├── episode_decoration_policy.proto │ │ │ ├── folder_decoration_policy.proto │ │ │ ├── playlist_album_decoration_policy.proto │ │ │ ├── playlist_decoration_policy.proto │ │ │ ├── playlist_episode_decoration_policy.proto │ │ │ ├── playlist_request_decoration_policy.proto │ │ │ ├── playlist_track_decoration_policy.proto │ │ │ ├── rootlist_folder_decoration_policy.proto │ │ │ ├── rootlist_playlist_decoration_policy.proto │ │ │ ├── rootlist_request_decoration_policy.proto │ │ │ ├── show_decoration_policy.proto │ │ │ ├── supported_link_types_in_playlists.proto │ │ │ ├── track_decoration_policy.proto │ │ │ └── user_decoration_policy.proto │ │ ├── popcount2_external.proto │ │ ├── prepare_play_options.proto │ │ ├── profile_cosmos.proto │ │ ├── profile_service.proto │ │ ├── property_definition.proto │ │ ├── protobuf_delta.proto │ │ ├── pubsub.proto │ │ ├── queue.proto │ │ ├── rate_limited_events.proto │ │ ├── rcs.proto │ │ ├── recently_played.proto │ │ ├── recently_played_backend.proto │ │ ├── record_id.proto │ │ ├── remote.proto │ │ ├── repeating_track_node.proto │ │ ├── request_failure.proto │ │ ├── resolve.proto │ │ ├── resource_type.proto │ │ ├── response_status.proto │ │ ├── restrictions.proto │ │ ├── resume_points_node.proto │ │ ├── rootlist_request.proto │ │ ├── seek_to_position.proto │ │ ├── sequence_number_entity.proto │ │ ├── session.proto │ │ ├── set_member_permission_request.proto │ │ ├── show_access.proto │ │ ├── show_episode_state.proto │ │ ├── show_offline_state.proto │ │ ├── show_request.proto │ │ ├── show_show_state.proto │ │ ├── signal-model.proto │ │ ├── skip_to_track.proto │ │ ├── social_connect_v2.proto │ │ ├── social_service.proto │ │ ├── socialgraph_response_status.proto │ │ ├── socialgraphv2.proto │ │ ├── spirc.proto │ │ ├── spotify/ │ │ │ ├── audiobookcashier/ │ │ │ │ └── v1/ │ │ │ │ └── audiobook_price.proto │ │ │ ├── clienttoken/ │ │ │ │ └── v0/ │ │ │ │ └── clienttoken_http.proto │ │ │ └── login5/ │ │ │ └── v3/ │ │ │ ├── challenges/ │ │ │ │ ├── code.proto │ │ │ │ └── hashcash.proto │ │ │ ├── client_info.proto │ │ │ ├── credentials/ │ │ │ │ └── credentials.proto │ │ │ ├── identifiers/ │ │ │ │ └── identifiers.proto │ │ │ ├── login5.proto │ │ │ └── user_info.proto │ │ ├── state_restore/ │ │ │ ├── ads_rules_inject_tracks.proto │ │ │ ├── automix_rules.proto │ │ │ ├── automix_talk_rules.proto │ │ │ ├── behavior_metadata_rules.proto │ │ │ ├── circuit_breaker_rules.proto │ │ │ ├── context_loader.proto │ │ │ ├── context_player_restorable.proto │ │ │ ├── context_player_rules.proto │ │ │ ├── context_player_rules_base.proto │ │ │ ├── context_player_state.proto │ │ │ ├── explicit_content_rules.proto │ │ │ ├── explicit_request_rules.proto │ │ │ ├── kitteh_box_rules.proto │ │ │ ├── mft_context_history.proto │ │ │ ├── mft_context_switch_rules.proto │ │ │ ├── mft_fallback_page_history.proto │ │ │ ├── mft_rules.proto │ │ │ ├── mft_rules_core.proto │ │ │ ├── mft_rules_inject_filler_tracks.proto │ │ │ ├── mft_state.proto │ │ │ ├── mod_interruption_state.proto │ │ │ ├── mod_rules_interruptions.proto │ │ │ ├── music_injection_rules.proto │ │ │ ├── playback_state.proto │ │ │ ├── player_model.proto │ │ │ ├── player_session.proto │ │ │ ├── player_session_fake.proto │ │ │ ├── player_session_queue.proto │ │ │ ├── provided_track.proto │ │ │ ├── random_source.proto │ │ │ ├── remove_banned_tracks_rules.proto │ │ │ ├── resume_points_rules.proto │ │ │ └── track_error_rules.proto │ │ ├── status.proto │ │ ├── status_code.proto │ │ ├── status_response.proto │ │ ├── storage-resolve.proto │ │ ├── storage_cosmos.proto │ │ ├── storylines.proto │ │ ├── stream_end_request.proto │ │ ├── stream_handle.proto │ │ ├── stream_progress_request.proto │ │ ├── stream_seek_request.proto │ │ ├── stream_start_request.proto │ │ ├── stream_start_response.proto │ │ ├── streaming_rule.proto │ │ ├── suppressions.proto │ │ ├── sync/ │ │ │ ├── album_sync_state.proto │ │ │ ├── artist_sync_state.proto │ │ │ ├── episode_sync_state.proto │ │ │ └── track_sync_state.proto │ │ ├── sync_request.proto │ │ ├── techu_core_exercise_cosmos.proto │ │ ├── track_instance.proto │ │ ├── track_instantiator.proto │ │ ├── transcripts.proto │ │ ├── transfer_node.proto │ │ ├── transfer_state.proto │ │ ├── tts-resolve.proto │ │ ├── ucs.proto │ │ ├── unfinished_episodes_request.proto │ │ ├── user_attributes.proto │ │ ├── useraccount.proto │ │ ├── your_library_config.proto │ │ ├── your_library_contains_request.proto │ │ ├── your_library_contains_response.proto │ │ ├── your_library_decorate_request.proto │ │ ├── your_library_decorate_response.proto │ │ ├── your_library_decorated_entity.proto │ │ ├── your_library_entity.proto │ │ ├── your_library_index.proto │ │ ├── your_library_pseudo_playlist_config.proto │ │ ├── your_library_request.proto │ │ └── your_library_response.proto │ └── src/ │ ├── impl_trait/ │ │ ├── context.rs │ │ └── player.rs │ ├── impl_trait.rs │ └── lib.rs ├── rust-toolchain.toml ├── rustfmt.toml ├── src/ │ ├── lib.rs │ ├── main.rs │ └── player_event_handler.rs └── test.sh
SYMBOL INDEX (1697 symbols across 122 files)
FILE: audio/src/decrypt.rs
type Aes128Ctr (line 5) | type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
constant AUDIO_AESIV (line 9) | const AUDIO_AESIV: [u8; 16] = [
type AudioDecrypt (line 13) | pub struct AudioDecrypt<T: io::Read> {
function new (line 20) | pub fn new(key: Option<AudioKey>, reader: T) -> AudioDecrypt<T> {
function read (line 33) | fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
function seek (line 45) | fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
FILE: audio/src/fetch/mod.rs
type AudioFileResult (line 29) | pub type AudioFileResult = Result<(), librespot_core::Error>;
constant DOWNLOAD_STATUS_POISON_MSG (line 31) | const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex sh...
type AudioFileError (line 34) | pub enum AudioFileError {
method from (line 50) | fn from(err: AudioFileError) -> Self {
type AudioFetchParams (line 63) | pub struct AudioFetchParams {
method set (line 123) | pub fn set(params: AudioFetchParams) -> Result<(), AudioFetchParams> {
method get (line 127) | pub fn get() -> &'static AudioFetchParams {
method default (line 102) | fn default() -> Self {
type AudioFile (line 132) | pub enum AudioFile {
method open (line 380) | pub async fn open(
method get_stream_loader_controller (line 415) | pub fn get_stream_loader_controller(&self) -> Result<StreamLoaderContr...
method is_cached (line 432) | pub fn is_cached(&self) -> bool {
type StreamingRequest (line 138) | pub struct StreamingRequest {
type StreamLoaderCommand (line 146) | pub enum StreamLoaderCommand {
type StreamLoaderController (line 152) | pub struct StreamLoaderController {
method len (line 159) | pub fn len(&self) -> usize {
method is_empty (line 163) | pub fn is_empty(&self) -> bool {
method range_available (line 167) | pub fn range_available(&self, range: Range) -> bool {
method range_to_end_available (line 183) | pub fn range_to_end_available(&self) -> bool {
method ping_time (line 193) | pub fn ping_time(&self) -> Option<Duration> {
method send_stream_loader_command (line 197) | fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
method fetch (line 205) | pub fn fetch(&self, range: Range) {
method fetch_blocking (line 210) | pub fn fetch_blocking(&self, mut range: Range) -> AudioFileResult {
method fetch_next_and_wait (line 260) | pub fn fetch_next_and_wait(
method set_random_access_mode (line 285) | pub fn set_random_access_mode(&self) {
method set_stream_mode (line 292) | pub fn set_stream_mode(&self) {
method close (line 299) | pub fn close(&self) {
method from_local_file (line 304) | pub fn from_local_file(file_size: u64) -> Self {
type AudioFileStreaming (line 313) | pub struct AudioFileStreaming {
method open (line 438) | pub async fn open(
type AudioFileDownloadStatus (line 320) | struct AudioFileDownloadStatus {
type AudioFileShared (line 325) | struct AudioFileShared {
method is_download_streaming (line 339) | fn is_download_streaming(&self) -> bool {
method set_download_streaming (line 343) | fn set_download_streaming(&self, streaming: bool) {
method ping_time (line 347) | fn ping_time(&self) -> Duration {
method set_ping_time (line 356) | fn set_ping_time(&self, duration: Duration) {
method throughput (line 361) | fn throughput(&self) -> usize {
method set_throughput (line 365) | fn set_throughput(&self, throughput: usize) {
method read_position (line 369) | fn read_position(&self) -> usize {
method set_read_position (line 373) | fn set_read_position(&self, position: u64) {
method read (line 553) | fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
method seek (line 629) | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
method read (line 674) | fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
method seek (line 683) | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
FILE: audio/src/fetch/receive.rs
type PartialFileData (line 24) | struct PartialFileData {
type ReceivedData (line 29) | enum ReceivedData {
constant ONE_SECOND (line 35) | const ONE_SECOND: Duration = Duration::from_secs(1);
constant DOWNLOAD_STATUS_POISON_MSG (line 36) | const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex sh...
function receive_data (line 38) | async fn receive_data(
type AudioFileFetch (line 154) | struct AudioFileFetch {
method has_download_slots_available (line 174) | fn has_download_slots_available(&self) -> bool {
method download_range (line 178) | fn download_range(&mut self, offset: usize, mut length: usize) -> Audi...
method pre_fetch_more_data (line 237) | fn pre_fetch_more_data(&mut self, bytes: usize) -> AudioFileResult {
method handle_file_data (line 276) | fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFl...
method handle_stream_loader_command (line 390) | fn handle_stream_loader_command(
method finish (line 404) | fn finish(&mut self) -> AudioFileResult {
type ControlFlow (line 168) | enum ControlFlow {
function audio_file_fetch (line 422) | pub(super) async fn audio_file_fetch(
FILE: audio/src/range_set.rs
type Range (line 8) | pub struct Range {
method fmt (line 14) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method new (line 20) | pub fn new(start: usize, length: usize) -> Range {
method end (line 24) | pub fn end(&self) -> usize {
type RangeSet (line 30) | pub struct RangeSet {
method fmt (line 35) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method new (line 45) | pub fn new() -> RangeSet {
method is_empty (line 51) | pub fn is_empty(&self) -> bool {
method len (line 55) | pub fn len(&self) -> usize {
method get_range (line 59) | pub fn get_range(&self, index: usize) -> Range {
method iter (line 63) | pub fn iter(&self) -> Iter<'_, Range> {
method contains (line 67) | pub fn contains(&self, value: usize) -> bool {
method contained_length_from_value (line 78) | pub fn contained_length_from_value(&self, value: usize) -> usize {
method contains_range_set (line 90) | pub fn contains_range_set(&self, other: &RangeSet) -> bool {
method add_range (line 99) | pub fn add_range(&mut self, range: &Range) {
method add_range_set (line 136) | pub fn add_range_set(&mut self, other: &RangeSet) {
method union (line 143) | pub fn union(&self, other: &RangeSet) -> RangeSet {
method subtract_range (line 149) | pub fn subtract_range(&mut self, range: &Range) {
method subtract_range_set (line 197) | pub fn subtract_range_set(&mut self, other: &RangeSet) {
method minus (line 203) | pub fn minus(&self, other: &RangeSet) -> RangeSet {
method intersection (line 209) | pub fn intersection(&self, other: &RangeSet) -> RangeSet {
FILE: connect/src/context_resolver.rs
type Resolve (line 20) | enum Resolve {
type ContextAction (line 26) | pub(super) enum ContextAction {
type ResolveContext (line 32) | pub(super) struct ResolveContext {
method append_context (line 40) | fn append_context(uri: impl Into<String>) -> Self {
method from_uri (line 49) | pub fn from_uri(
method from_context (line 64) | pub fn from_context(context: Context, update: ContextType, action: Con...
method resolve_uri (line 74) | fn resolve_uri(&self) -> Option<&str> {
method context_uri (line 87) | fn context_uri(&self) -> &str {
method fmt (line 96) | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
type ContextResolverError (line 108) | enum ContextResolverError {
method from (line 118) | fn from(value: ContextResolverError) -> Self {
type ContextResolver (line 123) | pub struct ContextResolver {
method new (line 133) | pub fn new(session: Session) -> Self {
method add (line 141) | pub fn add(&mut self, resolve: ResolveContext) {
method add_list (line 174) | pub fn add_list(&mut self, resolve: Vec<ResolveContext>) {
method remove_used_and_invalid (line 180) | pub fn remove_used_and_invalid(&mut self) {
method clear (line 187) | pub fn clear(&mut self) {
method find_next (line 191) | fn find_next(&self) -> Option<(&ResolveContext, &str, usize)> {
method has_next (line 205) | pub fn has_next(&self) -> bool {
method get_next_context (line 209) | pub async fn get_next_context(
method mark_next_unavailable (line 244) | pub fn mark_next_unavailable(&mut self) {
method apply_next_context (line 251) | pub fn apply_next_context(
method try_finish (line 284) | pub fn try_finish(
constant RETRY_UNAVAILABLE (line 130) | const RETRY_UNAVAILABLE: Duration = Duration::from_secs(3600);
FILE: connect/src/model.rs
type LoadRequest (line 9) | pub struct LoadRequest {
method from_context_uri (line 92) | pub fn from_context_uri(context_uri: String, options: LoadRequestOptio...
method from_tracks (line 103) | pub fn from_tracks(tracks: Vec<String>, options: LoadRequestOptions) -...
type Target (line 15) | type Target = LoadRequestOptions;
method deref (line 17) | fn deref(&self) -> &Self::Target {
type PlayContext (line 23) | pub(super) enum PlayContext {
type LoadRequestOptions (line 30) | pub struct LoadRequestOptions {
type LoadContextOptions (line 54) | pub enum LoadContextOptions {
type Options (line 66) | pub struct Options {
method from (line 76) | fn from(value: ContextPlayerOptionOverrides) -> Self {
type PlayingTrack (line 113) | pub enum PlayingTrack {
type Error (line 133) | type Error = ();
method try_from (line 135) | fn try_from(value: SkipTo) -> Result<Self, Self::Error> {
type SpircPlayStatus (line 151) | pub(super) enum SpircPlayStatus {
FILE: connect/src/shuffle_vec.rs
type ShuffleVec (line 8) | pub struct ShuffleVec<T> {
method eq (line 20) | fn eq(&self, other: &Self) -> bool {
type Target (line 26) | type Target = Vec<T>;
method deref (line 28) | fn deref(&self) -> &Self::Target {
method deref_mut (line 34) | fn deref_mut(&mut self) -> &mut Self::Target {
type Item (line 40) | type Item = T;
type IntoIter (line 41) | type IntoIter = IntoIter<T>;
method into_iter (line 43) | fn into_iter(self) -> Self::IntoIter {
function from (line 49) | fn from(vec: Vec<T>) -> Self {
function shuffle_with_seed (line 59) | pub fn shuffle_with_seed<F: Fn(&T) -> bool>(&mut self, seed: u64, is_fir...
function shuffle_with_rng (line 63) | pub fn shuffle_with_rng<F: Fn(&T) -> bool>(&mut self, mut rng: impl Rng,...
function unshuffle (line 92) | pub fn unshuffle(&mut self) {
function base (line 118) | fn base(range: Range<usize>) -> (ShuffleVec<usize>, u64) {
function test_shuffle_without_first (line 126) | fn test_shuffle_without_first() {
function test_shuffle_with_first (line 150) | fn test_shuffle_with_first() {
FILE: connect/src/spirc.rs
type SpircError (line 48) | enum SpircError {
method from (line 62) | fn from(err: SpircError) -> Self {
type SpircTask (line 72) | struct SpircTask {
method run (line 441) | async fn run(mut self) {
method handle_next_context (line 607) | fn handle_next_context(&mut self, next_context: Result<Context, Error>...
method emit_set_queue_event (line 654) | fn emit_set_queue_event(&self) {
method now_ms (line 691) | fn now_ms(&self) -> i64 {
method handle_command (line 699) | async fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Er...
method handle_player_event (line 755) | fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Er...
method handle_connection_id_update (line 888) | async fn handle_connection_id_update(&mut self, connection_id: String)...
method handle_user_attributes_update (line 940) | fn handle_user_attributes_update(&mut self, update: UserAttributesUpda...
method handle_user_attributes_mutation (line 950) | fn handle_user_attributes_mutation(&mut self, mutation: UserAttributes...
method handle_cluster_update (line 986) | async fn handle_cluster_update(
method handle_connect_state_request (line 1018) | async fn handle_connect_state_request(
method handle_request (line 1040) | async fn handle_request(&mut self, request: Request) -> Result<(), Err...
method handle_transfer (line 1167) | fn handle_transfer(&mut self, mut transfer: TransferState) -> Result<(...
method handle_disconnect (line 1289) | async fn handle_disconnect(&mut self) -> Result<(), Error> {
method handle_stop (line 1305) | fn handle_stop(&mut self) {
method handle_activate (line 1315) | fn handle_activate(&mut self) {
method handle_load (line 1344) | async fn handle_load(
method load_context_from_uri (line 1448) | async fn load_context_from_uri(
method load_context_from_tracks (line 1496) | fn load_context_from_tracks(&mut self, tracks: impl Into<ContextPage>)...
method handle_play (line 1515) | fn handle_play(&mut self) {
method handle_play_pause (line 1542) | fn handle_play_pause(&mut self) {
method handle_pause (line 1554) | fn handle_pause(&mut self) {
method handle_seek (line 1577) | fn handle_seek(&mut self, position_ms: u32) {
method handle_shuffle (line 1607) | fn handle_shuffle(&mut self, shuffle: bool) -> Result<(), Error> {
method handle_repeat_context (line 1612) | fn handle_repeat_context(&mut self, repeat: bool) -> Result<(), Error> {
method handle_repeat_track (line 1618) | fn handle_repeat_track(&mut self, repeat: bool) {
method handle_add_to_queue (line 1624) | async fn handle_add_to_queue(&mut self, uri: SpotifyUri) {
method handle_preload_next_track (line 1654) | fn handle_preload_next_track(&mut self) {
method handle_unavailable (line 1676) | fn handle_unavailable(&mut self, track_id: &SpotifyUri) -> Result<(), ...
method add_autoplay_resolving_when_required (line 1683) | fn add_autoplay_resolving_when_required(&mut self) {
method handle_next (line 1717) | fn handle_next(&mut self, track_uri: Option<String>) -> Result<(), Err...
method handle_prev (line 1747) | fn handle_prev(&mut self) -> Result<(), Error> {
method handle_volume_up (line 1768) | fn handle_volume_up(&mut self) {
method handle_volume_down (line 1775) | fn handle_volume_down(&mut self) {
method handle_playlist_modification (line 1782) | fn handle_playlist_modification(
method handle_session_update (line 1809) | fn handle_session_update(&mut self, session_update: FallbackWrapper<Se...
method position (line 1861) | fn position(&mut self) -> u32 {
method load_track (line 1873) | fn load_track(&mut self, start_playing: bool, position_ms: u32) -> Res...
method notify (line 1896) | async fn notify(&mut self) -> Result<(), Error> {
method set_volume (line 1912) | fn set_volume(&mut self, volume: u16) {
type SpircCommand (line 120) | enum SpircCommand {
constant CONTEXT_FETCH_THRESHOLD (line 141) | const CONTEXT_FETCH_THRESHOLD: usize = 2;
constant VOLUME_UPDATE_DELAY (line 144) | const VOLUME_UPDATE_DELAY: Duration = Duration::from_millis(500);
constant UPDATE_STATE_DELAY (line 146) | const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200);
type Spirc (line 149) | pub struct Spirc {
method new (line 160) | pub async fn new(
method shutdown (line 289) | pub fn shutdown(&self) -> Result<(), Error> {
method play (line 296) | pub fn play(&self) -> Result<(), Error> {
method play_pause (line 303) | pub fn play_pause(&self) -> Result<(), Error> {
method pause (line 310) | pub fn pause(&self) -> Result<(), Error> {
method prev (line 320) | pub fn prev(&self) -> Result<(), Error> {
method next (line 327) | pub fn next(&self) -> Result<(), Error> {
method volume_up (line 334) | pub fn volume_up(&self) -> Result<(), Error> {
method volume_down (line 341) | pub fn volume_down(&self) -> Result<(), Error> {
method shuffle (line 352) | pub fn shuffle(&self, shuffle: bool) -> Result<(), Error> {
method repeat (line 359) | pub fn repeat(&self, repeat: bool) -> Result<(), Error> {
method repeat_track (line 368) | pub fn repeat_track(&self, repeat: bool) -> Result<(), Error> {
method set_volume (line 375) | pub fn set_volume(&self, volume: u16) -> Result<(), Error> {
method set_position_ms (line 385) | pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> {
method load (line 394) | pub fn load(&self, command: LoadRequest) -> Result<(), Error> {
method add_to_queue (line 403) | pub fn add_to_queue(&self, uri: SpotifyUri) -> Result<(), Error> {
method disconnect (line 419) | pub fn disconnect(&self, pause: bool) -> Result<(), Error> {
method activate (line 426) | pub fn activate(&self) -> Result<(), Error> {
method transfer (line 433) | pub fn transfer(&self, transfer_request: Option<TransferRequest>) -> R...
method drop (line 1933) | fn drop(&mut self) {
FILE: connect/src/state.rs
constant SPOTIFY_MAX_PREV_TRACKS_SIZE (line 40) | const SPOTIFY_MAX_PREV_TRACKS_SIZE: usize = 10;
constant SPOTIFY_MAX_NEXT_TRACKS_SIZE (line 41) | const SPOTIFY_MAX_NEXT_TRACKS_SIZE: usize = 80;
type StateError (line 44) | pub(super) enum StateError {
method from (line 65) | fn from(err: StateError) -> Self {
type ConnectConfig (line 80) | pub struct ConnectConfig {
method default (line 98) | fn default() -> Self {
type ConnectState (line 112) | pub(super) struct ConnectState {
method new (line 140) | pub fn new(cfg: ConnectConfig, session: &Session) -> Self {
method reset (line 218) | fn reset(&mut self) {
method device_mut (line 239) | fn device_mut(&mut self) -> &mut Device {
method player_mut (line 246) | fn player_mut(&mut self) -> &mut PlayerState {
method device_info (line 253) | pub fn device_info(&self) -> &DeviceInfo {
method player (line 257) | pub fn player(&self) -> &PlayerState {
method is_active (line 261) | pub fn is_active(&self) -> bool {
method is_playing (line 268) | pub fn is_playing(&self) -> bool {
method is_pause (line 276) | pub fn is_pause(&self) -> bool {
method set_volume (line 281) | pub fn set_volume(&mut self, volume: u32) {
method set_last_command (line 289) | pub fn set_last_command(&mut self, command: Request) {
method set_now (line 294) | pub fn set_now(&mut self, now: u64) {
method set_active (line 307) | pub fn set_active(&mut self, value: bool) {
method set_origin (line 321) | pub fn set_origin(&mut self, origin: PlayOrigin) {
method set_session_id (line 325) | pub fn set_session_id(&mut self, session_id: String) {
method set_status (line 329) | pub(crate) fn set_status(&mut self, status: &SpircPlayStatus) {
method update_current_index (line 366) | pub fn update_current_index(&mut self, f: impl Fn(&mut ContextIndex)) {
method update_position (line 377) | pub fn update_position(&mut self, position_ms: u32, timestamp: i64) {
method update_duration (line 383) | pub fn update_duration(&mut self, duration: u32) {
method update_queue_revision (line 387) | pub fn update_queue_revision(&mut self) {
method reset_playback_to_position (line 395) | pub fn reset_playback_to_position(&mut self, new_index: Option<usize>)...
method mark_as_unavailable_for_match (line 435) | fn mark_as_unavailable_for_match(track: &mut ProvidedTrack, uri: &str) {
method update_position_in_relation (line 442) | pub fn update_position_in_relation(&mut self, timestamp: i64) {
method became_inactive (line 462) | pub async fn became_inactive(&mut self, session: &Session) -> SpClient...
method send_with_reason (line 469) | async fn send_with_reason(
method notify_new_device_appeared (line 484) | pub async fn notify_new_device_appeared(&mut self, session: &Session) ...
method notify_volume_changed (line 490) | pub async fn notify_volume_changed(&mut self, session: &Session) -> Sp...
method send_state (line 496) | pub async fn send_state(&self, session: &Session) -> SpClientResult {
FILE: connect/src/state/context.rs
constant LOCAL_FILES_IDENTIFIER (line 21) | const LOCAL_FILES_IDENTIFIER: &str = "spotify:local-files";
constant SEARCH_IDENTIFIER (line 22) | const SEARCH_IDENTIFIER: &str = "spotify:search";
type StateContext (line 25) | pub struct StateContext {
type ContextType (line 34) | pub enum ContextType {
type ResetContext (line 40) | pub enum ResetContext<'s> {
function page_url_to_uri (line 52) | fn page_url_to_uri(page_url: &str) -> String {
method find_index_in_context (line 68) | pub fn find_index_in_context<F: Fn(&ProvidedTrack) -> bool>(
method get_context (line 78) | pub fn get_context(&self, ty: ContextType) -> Result<&StateContext, Stat...
method get_context_mut (line 86) | pub fn get_context_mut(&mut self, ty: ContextType) -> Result<&mut StateC...
method context_uri (line 94) | pub fn context_uri(&self) -> &String {
method different_context_uri (line 98) | fn different_context_uri(&self, uri: &str) -> bool {
method reset_context (line 103) | pub fn reset_context(&mut self, mut reset_as: ResetContext) {
method valid_resolve_uri (line 140) | pub fn valid_resolve_uri(uri: &str) -> Option<&str> {
method find_valid_uri (line 148) | pub fn find_valid_uri<'s>(
method set_active_context (line 157) | pub fn set_active_context(&mut self, new_context: ContextType) {
method update_context (line 188) | pub fn update_context(
method find_first_prev_track_index (line 300) | fn find_first_prev_track_index(&self, ctx: &StateContext) -> Option<usiz...
method find_last_index_in_new_context (line 311) | fn find_last_index_in_new_context(
method state_context_from_page (line 346) | fn state_context_from_page(
method is_skip_track (line 386) | pub fn is_skip_track(&self, track: &ProvidedTrack, iteration: Option<u32...
method merge_context (line 399) | pub fn merge_context(&mut self, new_page: Option<ContextPage>) -> Option...
method update_context_index (line 429) | pub(super) fn update_context_index(
method context_to_provided_track (line 440) | pub fn context_to_provided_track(
method fill_context_from_page (line 505) | pub fn fill_context_from_page(&mut self, page: ContextPage) -> Result<()...
FILE: connect/src/state/handle.rs
method handle_shuffle (line 12) | pub fn handle_shuffle(&mut self, shuffle: bool) -> Result<(), Error> {
method handle_set_queue (line 37) | pub fn handle_set_queue(&mut self, set_queue: SetQueueCommand) {
method handle_set_repeat_context (line 43) | pub fn handle_set_repeat_context(&mut self, repeat: bool) -> Result<(), ...
FILE: connect/src/state/metadata.rs
constant CONTEXT_URI (line 8) | const CONTEXT_URI: &str = "context_uri";
constant ENTITY_URI (line 9) | const ENTITY_URI: &str = "entity_uri";
constant IS_QUEUED (line 10) | const IS_QUEUED: &str = "is_queued";
constant IS_AUTOPLAY (line 11) | const IS_AUTOPLAY: &str = "autoplay.is_autoplay";
constant HIDDEN (line 12) | const HIDDEN: &str = "hidden";
constant ITERATION (line 13) | const ITERATION: &str = "iteration";
constant CUSTOM_CONTEXT_INDEX (line 15) | const CUSTOM_CONTEXT_INDEX: &str = "context_index";
constant CUSTOM_SHUFFLE_SEED (line 16) | const CUSTOM_SHUFFLE_SEED: &str = "shuffle_seed";
constant CUSTOM_INITIAL_TRACK (line 17) | const CUSTOM_INITIAL_TRACK: &str = "initial_track";
type Metadata (line 41) | pub(super) trait Metadata {
method metadata (line 42) | fn metadata(&self) -> &HashMap<String, String>;
method metadata_mut (line 44) | fn metadata_mut(&mut self) -> &mut HashMap<String, String>;
method get_bool (line 46) | fn get_bool(&self, entry: &str) -> bool {
method get_usize (line 50) | fn get_usize(&self, entry: &str) -> Option<usize> {
method get (line 54) | fn get(&self, entry: &str) -> Option<&String> {
FILE: connect/src/state/options.rs
type ShuffleState (line 14) | pub(crate) struct ShuffleState {
method add_options_if_empty (line 20) | fn add_options_if_empty(&mut self) {
method set_repeat_context (line 26) | pub fn set_repeat_context(&mut self, repeat: bool) {
method set_repeat_track (line 33) | pub fn set_repeat_track(&mut self, repeat: bool) {
method set_shuffle (line 40) | pub fn set_shuffle(&mut self, shuffle: bool) {
method reset_options (line 47) | pub fn reset_options(&mut self) {
method validate_shuffle_allowed (line 53) | fn validate_shuffle_allowed(&self) -> Result<(), Error> {
method shuffle_restore (line 69) | pub fn shuffle_restore(&mut self, shuffle_state: ShuffleState) -> Result...
method shuffle_new (line 75) | pub fn shuffle_new(&mut self) -> Result<(), Error> {
method shuffle (line 84) | fn shuffle(&mut self, seed: u64, initial_track: &str) -> Result<(), Erro...
method shuffling_context (line 102) | pub fn shuffling_context(&self) -> bool {
method repeat_context (line 106) | pub fn repeat_context(&self) -> bool {
method repeat_track (line 110) | pub fn repeat_track(&self) -> bool {
FILE: connect/src/state/provider.rs
constant PROVIDER_CONTEXT (line 5) | const PROVIDER_CONTEXT: &str = "context";
constant PROVIDER_QUEUE (line 6) | const PROVIDER_QUEUE: &str = "queue";
constant PROVIDER_AUTOPLAY (line 7) | const PROVIDER_AUTOPLAY: &str = "autoplay";
constant PROVIDER_UNAVAILABLE (line 12) | const PROVIDER_UNAVAILABLE: &str = "unavailable";
type Provider (line 15) | pub enum Provider {
method fmt (line 23) | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
type IsProvider (line 37) | pub trait IsProvider {
method is_autoplay (line 38) | fn is_autoplay(&self) -> bool;
method is_context (line 39) | fn is_context(&self) -> bool;
method is_queue (line 40) | fn is_queue(&self) -> bool;
method is_unavailable (line 41) | fn is_unavailable(&self) -> bool;
method set_provider (line 43) | fn set_provider(&mut self, provider: Provider);
method is_autoplay (line 47) | fn is_autoplay(&self) -> bool {
method is_context (line 51) | fn is_context(&self) -> bool {
method is_queue (line 55) | fn is_queue(&self) -> bool {
method is_unavailable (line 59) | fn is_unavailable(&self) -> bool {
method set_provider (line 63) | fn set_provider(&mut self, provider: Provider) {
FILE: connect/src/state/restrictions.rs
method clear_restrictions (line 7) | pub fn clear_restrictions(&mut self) {
method update_restrictions (line 14) | pub fn update_restrictions(&mut self) {
FILE: connect/src/state/tracks.rs
constant IDENTIFIER_DELIMITER (line 15) | pub const IDENTIFIER_DELIMITER: &str = "delimiter";
method new_delimiter (line 18) | fn new_delimiter(iteration: i64) -> ProvidedTrack {
method push_prev (line 31) | fn push_prev(&mut self, prev: ProvidedTrack) {
method get_next_track (line 41) | fn get_next_track(&mut self) -> Option<ProvidedTrack> {
method prev_tracks_mut (line 51) | fn prev_tracks_mut(&mut self) -> &mut Vec<ProvidedTrack> {
method prev_tracks (line 56) | pub(super) fn prev_tracks(&self) -> &Vec<ProvidedTrack> {
method next_tracks_mut (line 61) | fn next_tracks_mut(&mut self) -> &mut Vec<ProvidedTrack> {
method next_tracks (line 66) | pub(super) fn next_tracks(&self) -> &Vec<ProvidedTrack> {
method set_current_track_random (line 70) | pub fn set_current_track_random(&mut self) -> Result<(), Error> {
method set_current_track (line 76) | pub fn set_current_track(&mut self, index: usize) -> Result<(), Error> {
method next_track (line 105) | pub fn next_track(&mut self) -> Result<Option<u32>, Error> {
method prev_track (line 170) | pub fn prev_track(&mut self) -> Result<Option<&MessageField<ProvidedTrac...
method current_track (line 224) | pub fn current_track<F: Fn(&'ct MessageField<ProvidedTrack>) -> R, R>(
method set_track (line 231) | pub fn set_track(&mut self, track: ProvidedTrack) {
method set_next_tracks (line 235) | pub fn set_next_tracks(&mut self, mut tracks: Vec<ProvidedTrack>) {
method set_prev_tracks (line 261) | pub fn set_prev_tracks(&mut self, tracks: Vec<ProvidedTrack>) {
method clear_prev_track (line 265) | pub fn clear_prev_track(&mut self) {
method clear_next_tracks (line 269) | pub fn clear_next_tracks(&mut self) {
method fill_up_next_tracks (line 284) | pub fn fill_up_next_tracks(&mut self) -> Result<(), Error> {
method preview_next_track (line 355) | pub fn preview_next_track(&mut self) -> Option<SpotifyUri> {
method has_next_tracks (line 365) | pub fn has_next_tracks(&self, min: Option<usize>) -> bool {
method recent_track_uris (line 373) | pub fn recent_track_uris(&self) -> Vec<String> {
method mark_unavailable (line 384) | pub fn mark_unavailable(&mut self, id: &SpotifyUri) -> Result<(), Error> {
method add_to_queue (line 414) | pub fn add_to_queue(&mut self, mut track: ProvidedTrack, rev_update: boo...
FILE: connect/src/state/transfer.rs
method current_track_from_transfer (line 15) | pub fn current_track_from_transfer(
method handle_initial_transfer (line 42) | pub fn handle_initial_transfer(
method finish_transfer (line 121) | pub fn finish_transfer(&mut self, transfer: TransferState) -> Result<(),...
FILE: core/build.rs
function main (line 5) | fn main() -> Result<(), Box<dyn std::error::Error>> {
FILE: core/src/apresolve.rs
type SocketAddress (line 9) | pub type SocketAddress = (String, u16);
type AccessPoints (line 12) | pub struct AccessPoints {
method is_any_empty (line 39) | fn is_any_empty(&self) -> bool {
type ApResolveData (line 19) | pub struct ApResolveData {
method fallback (line 29) | fn fallback() -> Self {
method port_config (line 53) | pub fn port_config(&self) -> Option<u16> {
method process_ap_strings (line 61) | fn process_ap_strings(&self, data: Vec<String>) -> VecDeque<SocketAddres...
method parse_resolve_to_access_points (line 77) | fn parse_resolve_to_access_points(&self, resolve: ApResolveData) -> Acce...
method try_apresolve (line 85) | pub async fn try_apresolve(&self) -> Result<ApResolveData, Error> {
method apresolve (line 97) | async fn apresolve(&self) {
method is_any_empty (line 122) | fn is_any_empty(&self) -> bool {
method resolve (line 126) | pub async fn resolve(&self, endpoint: &str) -> Result<SocketAddress, Err...
FILE: core/src/audio_key.rs
type AudioKey (line 11) | pub struct AudioKey(pub [u8; 16]);
type AudioKeyError (line 14) | pub enum AudioKeyError {
method from (line 28) | fn from(err: AudioKeyError) -> Self {
method dispatch (line 47) | pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Resul...
method request (line 81) | pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<Au...
method send_key_request (line 101) | fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> ...
FILE: core/src/authentication.rs
type AuthenticationError (line 16) | pub enum AuthenticationError {
method from (line 24) | fn from(err: AuthenticationError) -> Self {
type Credentials (line 31) | pub struct Credentials {
method with_password (line 53) | pub fn with_password(username: impl Into<String>, password: impl Into<...
method with_access_token (line 61) | pub fn with_access_token(token: impl Into<String>) -> Self {
method with_blob (line 70) | pub fn with_blob(
function serialize_protobuf_enum (line 156) | fn serialize_protobuf_enum<T, S>(v: &T, ser: S) -> Result<S::Ok, S::Error>
function deserialize_protobuf_enum (line 164) | fn deserialize_protobuf_enum<'de, T, D>(de: D) -> Result<T, D::Error>
function serialize_base64 (line 173) | fn serialize_base64<T, S>(v: &T, ser: S) -> Result<S::Ok, S::Error>
function deserialize_base64 (line 181) | fn deserialize_base64<'de, D>(de: D) -> Result<Vec<u8>, D::Error>
FILE: core/src/cache.rs
constant CACHE_LIMITER_POISON_MSG (line 18) | const CACHE_LIMITER_POISON_MSG: &str = "cache limiter mutex should not b...
type CacheError (line 21) | pub enum CacheError {
method from (line 27) | fn from(err: CacheError) -> Self {
type SizeLimiter (line 35) | struct SizeLimiter {
method new (line 44) | fn new(limit: u64) -> Self {
method add (line 56) | fn add(&mut self, file: &Path, size: u64, accessed: SystemTime) {
method exceeds_limit (line 67) | fn exceeds_limit(&self) -> bool {
method pop (line 76) | fn pop(&mut self) -> Option<PathBuf> {
method update (line 95) | fn update(&mut self, file: &Path, access_time: SystemTime) -> bool {
method remove (line 102) | fn remove(&mut self, file: &Path) -> bool {
type FsSizeLimiter (line 117) | struct FsSizeLimiter {
method get_metadata (line 123) | fn get_metadata(file: &Path) -> io::Result<(SystemTime, u64)> {
method init_dir (line 143) | fn init_dir(limiter: &mut SizeLimiter, path: &Path) {
method add (line 194) | fn add(&self, file: &Path, size: u64) {
method touch (line 201) | fn touch(&self, file: &Path) -> bool {
method remove (line 208) | fn remove(&self, file: &Path) -> bool {
method prune_internal (line 215) | fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) -> Result...
method prune (line 246) | fn prune(&self) -> Result<(), Error> {
method new (line 250) | fn new(path: &Path, limit: u64) -> Result<Self, Error> {
type Cache (line 264) | pub struct Cache {
method new (line 272) | pub fn new<P: AsRef<Path>>(
method credentials (line 315) | pub fn credentials(&self) -> Option<Credentials> {
method save_credentials (line 343) | pub fn save_credentials(&self, cred: &Credentials) {
method volume (line 361) | pub fn volume(&self) -> Option<u16> {
method save_volume (line 382) | pub fn save_volume(&self, volume: u16) {
method file_path (line 391) | pub fn file_path(&self, file: FileId) -> Option<PathBuf> {
method file (line 400) | pub fn file(&self, file: FileId) -> Option<File> {
method save_file (line 420) | pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> Re...
method remove_file (line 438) | pub fn remove_file(&self, file: FileId) -> Result<(), Error> {
function ordered_time (line 455) | fn ordered_time(v: u64) -> SystemTime {
function test_size_limiter (line 460) | fn test_size_limiter() {
FILE: core/src/cdn_url.rs
type MaybeExpiringUrl (line 15) | pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
constant CDN_URL_EXPIRY_MARGIN (line 17) | const CDN_URL_EXPIRY_MARGIN: Duration = Duration::seconds(5 * 60);
type MaybeExpiringUrls (line 20) | pub struct MaybeExpiringUrls(pub Vec<MaybeExpiringUrl>);
type Error (line 130) | type Error = crate::Error;
method try_from (line 131) | fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
type Target (line 23) | type Target = Vec<MaybeExpiringUrl>;
method deref (line 24) | fn deref(&self) -> &Self::Target {
method deref_mut (line 30) | fn deref_mut(&mut self) -> &mut Self::Target {
type CdnUrlError (line 36) | pub enum CdnUrlError {
method from (line 46) | fn from(err: CdnUrlError) -> Self {
type CdnUrl (line 55) | pub struct CdnUrl {
method new (line 61) | pub fn new(file_id: FileId) -> Self {
method resolve_audio (line 68) | pub async fn resolve_audio(&self, session: &Session) -> Result<Self, E...
method try_get_url (line 82) | pub fn try_get_url(&self) -> Result<&str, Error> {
method try_get_urls (line 100) | pub fn try_get_urls(&self) -> Result<Vec<&str>, Error> {
function test_maybe_expiring_urls (line 225) | fn test_maybe_expiring_urls() {
FILE: core/src/channel.rs
constant ONE_SECOND (line 30) | const ONE_SECOND: Duration = Duration::from_secs(1);
type ChannelError (line 33) | pub struct ChannelError;
method fmt (line 42) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method from (line 36) | fn from(err: ChannelError) -> Self {
type Channel (line 47) | pub struct Channel {
method recv_packet (line 133) | fn recv_packet(&mut self, cx: &mut Context<'_>) -> Poll<Result<Bytes, ...
method split (line 149) | pub fn split(self) -> (ChannelHeaders, ChannelData) {
type ChannelHeaders (line 52) | pub struct ChannelHeaders(BiLock<Channel>);
type ChannelData (line 53) | pub struct ChannelData(BiLock<Channel>);
type ChannelEvent (line 55) | pub enum ChannelEvent {
type ChannelState (line 61) | enum ChannelState {
method allocate (line 68) | pub fn allocate(&self) -> (u16, Channel) {
method dispatch (line 87) | pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Resul...
method get_download_rate_estimate (line 119) | pub fn get_download_rate_estimate(&self) -> usize {
method shutdown (line 123) | pub(crate) fn shutdown(&self) {
type Item (line 157) | type Item = Result<ChannelEvent, ChannelError>;
method poll_next (line 159) | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Opt...
type Item (line 203) | type Item = Result<Bytes, ChannelError>;
method poll_next (line 205) | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<...
type Item (line 219) | type Item = Result<(u8, Vec<u8>), ChannelError>;
method poll_next (line 221) | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<...
FILE: core/src/component.rs
constant COMPONENT_POISON_MSG (line 1) | pub(crate) const COMPONENT_POISON_MSG: &str = "component mutex should no...
FILE: core/src/config.rs
constant KEYMASTER_CLIENT_ID (line 6) | pub(crate) const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233...
constant ANDROID_CLIENT_ID (line 7) | pub(crate) const ANDROID_CLIENT_ID: &str = "9a8d2f0ce77a4e248bb71fefcb55...
constant IOS_CLIENT_ID (line 8) | pub(crate) const IOS_CLIENT_ID: &str = "58bd3c95768941ea9eb4350aaa033eb3";
constant OS (line 14) | pub const OS: &str = std::env::consts::OS;
function os_version (line 20) | pub fn os_version() -> String {
type SessionConfig (line 25) | pub struct SessionConfig {
method default_for_os (line 35) | pub(crate) fn default_for_os(os: &str) -> Self {
method default (line 56) | fn default() -> Self {
type DeviceType (line 62) | pub enum DeviceType {
method fmt (line 142) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Err (line 85) | type Err = ();
method from_str (line 86) | fn from_str(s: &str) -> Result<Self, Self::Err> {
function from (line 110) | fn from(d: &DeviceType) -> &'static str {
function from (line 136) | fn from(d: DeviceType) -> &'static str {
method from (line 149) | fn from(value: DeviceType) -> Self {
FILE: core/src/connection/codec.rs
constant HEADER_SIZE (line 9) | const HEADER_SIZE: usize = 3;
constant MAC_SIZE (line 10) | const MAC_SIZE: usize = 4;
type ApCodecError (line 13) | pub enum ApCodecError {
type DecodeState (line 19) | enum DecodeState {
type ApCodec (line 24) | pub struct ApCodec {
method new (line 34) | pub fn new(send_key: &[u8], recv_key: &[u8]) -> ApCodec {
type Error (line 47) | type Error = io::Error;
method encode (line 49) | fn encode(&mut self, item: (u8, Vec<u8>), buf: &mut BytesMut) -> io::R...
type Item (line 72) | type Item = (u8, Bytes);
type Error (line 73) | type Error = io::Error;
method decode (line 75) | fn decode(&mut self, buf: &mut BytesMut) -> io::Result<Option<(u8, Bytes...
FILE: core/src/connection/handshake.rs
constant SERVER_KEY (line 22) | const SERVER_KEY: [u8; 256] = [
type HandshakeError (line 42) | pub enum HandshakeError {
function handshake (line 49) | pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
function client_hello (line 106) | async fn client_hello<T>(connection: &mut T, gc: Vec<u8>) -> io::Result<...
function client_response (line 200) | async fn client_response<T>(connection: &mut T, challenge: Vec<u8>) -> i...
function recv_packet (line 224) | async fn recv_packet<T, M>(connection: &mut T, acc: &mut Vec<u8>) -> io:...
function read_into_accumulator (line 236) | async fn read_into_accumulator<'b, T: AsyncRead + Unpin>(
function compute_keys (line 248) | fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> io::Result<(Vec...
FILE: core/src/connection/mod.rs
type Transport (line 20) | pub type Transport = Framed<TcpStream, ApCodec>;
function login_error_message (line 22) | fn login_error_message(code: &ErrorCode) -> &'static str {
type AuthenticationError (line 40) | pub enum AuthenticationError {
method from (line 60) | fn from(login_failure: APLoginFailed) -> Self {
method from (line 50) | fn from(err: AuthenticationError) -> Self {
function connect (line 65) | pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::...
function connect_with_retry (line 75) | pub async fn connect_with_retry(
function authenticate (line 98) | pub async fn authenticate(
FILE: core/src/date.rs
method from (line 14) | fn from(err: ComponentRange) -> Self {
type Date (line 20) | pub struct Date(pub OffsetDateTime);
method as_timestamp_ms (line 30) | pub fn as_timestamp_ms(&self) -> i64 {
method from_timestamp_ms (line 34) | pub fn from_timestamp_ms(timestamp: i64) -> Result<Self, Error> {
method as_utc (line 39) | pub fn as_utc(&self) -> OffsetDateTime {
method from_utc (line 43) | pub fn from_utc(date_time: PrimitiveDateTime) -> Self {
method now_utc (line 47) | pub fn now_utc() -> Self {
method from_iso8601 (line 51) | pub fn from_iso8601(input: &str) -> Result<Self, Error> {
type Error (line 58) | type Error = crate::Error;
method try_from (line 59) | fn try_from(msg: &DateMessage) -> Result<Self, Self::Error> {
method from (line 78) | fn from(datetime: OffsetDateTime) -> Self {
type Target (line 23) | type Target = OffsetDateTime;
method deref (line 24) | fn deref(&self) -> &Self::Target {
FILE: core/src/dealer/manager.rs
type BoxedStream (line 22) | pub type BoxedStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
type BoxedStreamResult (line 23) | pub type BoxedStreamResult<T> = BoxedStream<Result<T, Error>>;
type DealerError (line 26) | enum DealerError {
method from (line 36) | fn from(err: DealerError) -> Self {
type Reply (line 42) | pub enum Reply {
type RequestReply (line 48) | pub type RequestReply = (Request, mpsc::UnboundedSender<Reply>);
type RequestReceiver (line 49) | type RequestReceiver = mpsc::UnboundedReceiver<RequestReply>;
type RequestSender (line 50) | type RequestSender = mpsc::UnboundedSender<RequestReply>;
type DealerRequestHandler (line 52) | struct DealerRequestHandler(RequestSender);
method new (line 55) | pub fn new() -> (Self, RequestReceiver) {
method handle_request (line 62) | fn handle_request(&self, request: Request, responder: Responder) {
method get_url (line 85) | async fn get_url(session: Session) -> GetUrlResult {
method add_listen_for (line 93) | pub fn add_listen_for(&self, url: impl Into<String>) -> Result<Subscript...
method listen_for (line 106) | pub fn listen_for<T>(
method add_handle_for (line 114) | pub fn add_handle_for(&self, url: impl Into<String>) -> Result<RequestRe...
method handle_for (line 129) | pub fn handle_for(&self, uri: impl Into<String>) -> Result<BoxedStream<R...
method handles (line 135) | pub fn handles(&self, uri: &str) -> bool {
method start (line 147) | pub async fn start(&self) -> Result<(), Error> {
method close (line 169) | pub async fn close(&self) {
FILE: core/src/dealer/maps.rs
type HandlerMapError (line 7) | pub enum HandlerMapError {
method from (line 13) | fn from(err: HandlerMapError) -> Self {
type HandlerMap (line 18) | pub enum HandlerMap<T> {
method default (line 24) | fn default() -> Self {
function contains (line 30) | pub fn contains(&self, path: &str) -> bool {
function insert (line 34) | pub fn insert<'a>(
function get (line 55) | pub fn get<'a>(&self, mut path: impl Iterator<Item = &'a str>) -> Option...
function remove (line 65) | pub fn remove<'a>(&mut self, mut path: impl Iterator<Item = &'a str>) ->...
type SubscriberMap (line 87) | pub struct SubscriberMap<T> {
method default (line 93) | fn default() -> Self {
function insert (line 102) | pub fn insert<'a>(&mut self, mut path: impl Iterator<Item = &'a str>, ha...
function contains (line 113) | pub fn contains<'a>(&self, mut path: impl Iterator<Item = &'a str>) -> b...
function is_empty (line 129) | pub fn is_empty(&self) -> bool {
function retain (line 133) | pub fn retain<'a>(
FILE: core/src/dealer/mod.rs
type WsMessage (line 41) | type WsMessage = tungstenite::Message;
type WsError (line 42) | type WsError = tungstenite::Error;
type WsResult (line 43) | type WsResult<T> = Result<T, Error>;
type GetUrlResult (line 44) | type GetUrlResult = Result<Url, Error>;
method from (line 47) | fn from(err: WsError) -> Self {
constant WEBSOCKET_CLOSE_TIMEOUT (line 52) | const WEBSOCKET_CLOSE_TIMEOUT: Duration = Duration::from_secs(3);
constant PING_INTERVAL (line 54) | const PING_INTERVAL: Duration = Duration::from_secs(30);
constant PING_TIMEOUT (line 55) | const PING_TIMEOUT: Duration = Duration::from_secs(3);
constant RECONNECT_INTERVAL (line 57) | const RECONNECT_INTERVAL: Duration = Duration::from_secs(10);
constant DEALER_REQUEST_HANDLERS_POISON_MSG (line 59) | const DEALER_REQUEST_HANDLERS_POISON_MSG: &str =
constant DEALER_MESSAGE_HANDLERS_POISON_MSG (line 61) | const DEALER_MESSAGE_HANDLERS_POISON_MSG: &str =
type Response (line 64) | struct Response {
type Responder (line 68) | struct Responder {
method new (line 75) | fn new(key: String, tx: mpsc::UnboundedSender<WsMessage>) -> Self {
method send_internal (line 84) | fn send_internal(&mut self, response: Response) {
method send (line 99) | pub fn send(mut self, response: Response) {
method force_unanswered (line 104) | pub fn force_unanswered(mut self) {
method drop (line 110) | fn drop(&mut self) {
type IntoResponse (line 117) | trait IntoResponse {
method respond (line 118) | fn respond(self, responder: Responder);
method respond (line 122) | fn respond(self, responder: Responder) {
method respond (line 131) | fn respond(self, responder: Responder) {
type RequestHandler (line 148) | trait RequestHandler: Send + 'static {
method handle_request (line 143) | fn handle_request(&self, request: Request, responder: Responder) {
method handle_request (line 149) | fn handle_request(&self, request: Request, responder: Responder);
type MessageHandler (line 152) | type MessageHandler = mpsc::UnboundedSender<Message>;
type Subscription (line 156) | pub struct Subscription(UnboundedReceiver<Message>);
type Item (line 159) | type Item = Message;
method poll_next (line 161) | fn poll_next(
function split_uri (line 169) | fn split_uri(s: &str) -> Option<impl Iterator<Item = &'_ str>> {
type AddHandlerError (line 187) | enum AddHandlerError {
method from (line 195) | fn from(err: AddHandlerError) -> Self {
type SubscriptionError (line 204) | enum SubscriptionError {
method from (line 210) | fn from(err: SubscriptionError) -> Self {
function add_handler (line 215) | fn add_handler(
function remove_handler (line 224) | fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
function subscribe (line 228) | fn subscribe(
function handles (line 242) | fn handles(
type Builder (line 258) | struct Builder {
method new (line 288) | pub fn new() -> Self {
method add_handler (line 292) | pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler)...
method subscribe (line 296) | pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, Err...
method handles (line 300) | pub fn handles(&self, uri: &str) -> bool {
method launch_in_background (line 304) | pub fn launch_in_background<Fut, F>(self, get_url: F, proxy: Option<Ur...
method launch (line 312) | pub async fn launch<Fut, F>(self, get_url: F, proxy: Option<Url>) -> W...
type DealerShared (line 330) | struct DealerShared {
method dispatch_message (line 340) | fn dispatch_message(&self, mut msg: WebsocketMessage) {
method dispatch_request (line 367) | fn dispatch_request(
method dispatch (line 408) | fn dispatch(&self, m: MessageOrRequest, send_tx: &mpsc::UnboundedSende...
method closed (line 415) | async fn closed(&self) {
method is_closed (line 421) | fn is_closed(&self) -> bool {
type Dealer (line 426) | struct Dealer {
method add_handler (line 432) | pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), Error>
method remove_handler (line 447) | pub fn remove_handler(&self, uri: &str) -> Option<Box<dyn RequestHandl...
method subscribe (line 458) | pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, Error> {
method handles (line 469) | pub fn handles(&self, uri: &str) -> bool {
method close (line 485) | pub async fn close(mut self) {
function connect (line 499) | async fn connect(
function run (line 663) | async fn run<F, Fut>(
FILE: core/src/dealer/protocol.rs
constant IGNORE_UNKNOWN (line 16) | const IGNORE_UNKNOWN: protobuf_json_mapping::ParseOptions = protobuf_jso...
type JsonValue (line 21) | type JsonValue = serde_json::Value;
type ProtocolError (line 24) | enum ProtocolError {
method from (line 40) | fn from(err: ProtocolError) -> Self {
type Payload (line 49) | pub(super) struct Payload {
type WebsocketRequest (line 54) | pub(super) struct WebsocketRequest {
method handle_payload (line 150) | pub fn handle_payload(&self) -> Result<Request, Error> {
type WebsocketMessage (line 63) | pub(super) struct WebsocketMessage {
method handle_payload (line 129) | pub fn handle_payload(&mut self) -> Result<PayloadValue, Error> {
type MessagePayloadValue (line 74) | pub enum MessagePayloadValue {
type MessageOrRequest (line 82) | pub(super) enum MessageOrRequest {
type PayloadValue (line 88) | pub enum PayloadValue {
type Message (line 95) | pub struct Message {
method try_from_json (line 109) | pub fn try_from_json<M: protobuf::MessageFull>(
method from_raw (line 118) | pub fn from_raw<M: protobuf::Message>(value: Self) -> Result<M, Error> {
type FallbackWrapper (line 103) | pub enum FallbackWrapper<T: protobuf::MessageFull> {
function handle_transfer_encoding (line 172) | fn handle_transfer_encoding(
FILE: core/src/dealer/protocol/request.rs
type Request (line 15) | pub struct Request {
type Command (line 25) | pub enum Command {
method fmt (line 48) | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
type TransferCommand (line 84) | pub struct TransferCommand {
type PlayCommand (line 93) | pub struct PlayCommand {
type PauseCommand (line 103) | pub struct PauseCommand {
type SeekToCommand (line 109) | pub struct SeekToCommand {
type SkipNextCommand (line 116) | pub struct SkipNextCommand {
type SetValueCommand (line 123) | pub struct SetValueCommand {
type AddToQueueCommand (line 129) | pub struct AddToQueueCommand {
type SetQueueCommand (line 136) | pub struct SetQueueCommand {
type SetOptionsCommand (line 148) | pub struct SetOptionsCommand {
type UpdateContextCommand (line 157) | pub struct UpdateContextCommand {
type GenericCommand (line 164) | pub struct GenericCommand {
type TransferOptions (line 169) | pub struct TransferOptions {
type PlayOptions (line 181) | pub struct PlayOptions {
type OptionsOptions (line 197) | pub struct OptionsOptions {
type SkipTo (line 204) | pub struct SkipTo {
type LoggingParams (line 211) | pub struct LoggingParams {
FILE: core/src/deserialize_with.rs
constant IGNORE_UNKNOWN (line 8) | const IGNORE_UNKNOWN: protobuf_json_mapping::ParseOptions = protobuf_jso...
function parse_value_to_msg (line 13) | fn parse_value_to_msg<T: MessageFull>(
function base64_proto (line 19) | pub fn base64_proto<'de, T, D>(de: D) -> Result<Option<T>, D::Error>
function json_proto (line 32) | pub fn json_proto<'de, T, D>(de: D) -> Result<T, D::Error>
function option_json_proto (line 41) | pub fn option_json_proto<'de, T, D>(de: D) -> Result<Option<T>, D::Error>
function vec_json_proto (line 50) | pub fn vec_json_proto<'de, T, D>(de: D) -> Result<Vec<T>, D::Error>
function boxed (line 69) | pub fn boxed<'de, T, D>(de: D) -> Result<Box<T>, D::Error>
function bool_from_string (line 78) | pub fn bool_from_string<'de, D>(de: D) -> Result<bool, D::Error>
FILE: core/src/diffie_hellman.rs
function powm (line 21) | fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
type DhLocalKeys (line 37) | pub struct DhLocalKeys {
method random (line 43) | pub fn random<R: Rng + CryptoRng>(rng: &mut R) -> DhLocalKeys {
method public_key (line 55) | pub fn public_key(&self) -> Vec<u8> {
method shared_secret (line 59) | pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
FILE: core/src/error.rs
type Error (line 25) | pub struct Error {
method new (line 94) | pub fn new<E>(kind: ErrorKind, error: E) -> Error
method aborted (line 104) | pub fn aborted<E>(error: E) -> Error
method already_exists (line 114) | pub fn already_exists<E>(error: E) -> Error
method cancelled (line 124) | pub fn cancelled<E>(error: E) -> Error
method data_loss (line 134) | pub fn data_loss<E>(error: E) -> Error
method deadline_exceeded (line 144) | pub fn deadline_exceeded<E>(error: E) -> Error
method do_not_use (line 154) | pub fn do_not_use<E>(error: E) -> Error
method failed_precondition (line 164) | pub fn failed_precondition<E>(error: E) -> Error
method internal (line 174) | pub fn internal<E>(error: E) -> Error
method invalid_argument (line 184) | pub fn invalid_argument<E>(error: E) -> Error
method not_found (line 194) | pub fn not_found<E>(error: E) -> Error
method out_of_range (line 204) | pub fn out_of_range<E>(error: E) -> Error
method permission_denied (line 214) | pub fn permission_denied<E>(error: E) -> Error
method resource_exhausted (line 224) | pub fn resource_exhausted<E>(error: E) -> Error
method unauthenticated (line 234) | pub fn unauthenticated<E>(error: E) -> Error
method unavailable (line 244) | pub fn unavailable<E>(error: E) -> Error
method unimplemented (line 254) | pub fn unimplemented<E>(error: E) -> Error
method unknown (line 264) | pub fn unknown<E>(error: E) -> Error
method source (line 276) | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
method fmt (line 282) | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
method from (line 290) | fn from(err: OAuthError) -> Self {
method from (line 309) | fn from(err: DecodeError) -> Self {
method from (line 315) | fn from(err: http::Error) -> Self {
method from (line 334) | fn from(err: hyper::Error) -> Self {
method from (line 360) | fn from(err: hyper_util::client::legacy::Error) -> Self {
method from (line 370) | fn from(err: time::error::Parse) -> Self {
method from (line 376) | fn from(err: quick_xml::Error) -> Self {
method from (line 382) | fn from(err: serde_json::Error) -> Self {
method from (line 388) | fn from(err: std::io::Error) -> Self {
method from (line 417) | fn from(err: FromUtf8Error) -> Self {
method from (line 423) | fn from(err: InvalidHeaderValue) -> Self {
method from (line 429) | fn from(err: InvalidUri) -> Self {
method from (line 435) | fn from(err: ParseError) -> Self {
method from (line 441) | fn from(err: ParseIntError) -> Self {
method from (line 447) | fn from(err: TryFromIntError) -> Self {
method from (line 453) | fn from(err: ProtobufError) -> Self {
method from (line 459) | fn from(err: RecvError) -> Self {
method from (line 465) | fn from(err: SendError<T>) -> Self {
method from (line 474) | fn from(err: AcquireError) -> Self {
method from (line 483) | fn from(err: TryAcquireError) -> Self {
method from (line 492) | fn from(err: ToStrError) -> Self {
method from (line 498) | fn from(err: Utf8Error) -> Self {
method from (line 504) | fn from(err: protobuf_json_mapping::ParseError) -> Self {
type ErrorKind (line 31) | pub enum ErrorKind {
type ErrorMessage (line 85) | struct ErrorMessage(String);
method fmt (line 88) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
FILE: core/src/file_id.rs
constant RAW_LEN (line 5) | const RAW_LEN: usize = 20;
type FileId (line 8) | pub struct FileId(pub [u8; RAW_LEN]);
method from_raw (line 11) | pub fn from_raw(src: &[u8]) -> FileId {
method to_base16 (line 22) | pub fn to_base16(&self) -> String {
method fmt (line 32) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method fmt (line 38) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method from (line 44) | fn from(src: &[u8]) -> Self {
method from (line 49) | fn from(image: &protocol::metadata::Image) -> Self {
method from (line 55) | fn from(file: &protocol::metadata::AudioFile) -> Self {
method from (line 61) | fn from(video: &protocol::metadata::VideoFile) -> Self {
FILE: core/src/http_client.rs
constant RATE_LIMIT_INTERVAL (line 39) | pub const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(30);
constant RATE_LIMIT_MAX_WAIT (line 40) | pub const RATE_LIMIT_MAX_WAIT: Duration = Duration::from_secs(10);
constant RATE_LIMIT_CALLS_PER_INTERVAL (line 41) | pub const RATE_LIMIT_CALLS_PER_INTERVAL: u32 = 300;
type HttpClientError (line 44) | pub enum HttpClientError {
method from (line 50) | fn from(err: HttpClientError) -> Self {
type HyperClient (line 95) | type HyperClient = Client<ProxyConnector<HttpsConnector<HttpConnector>>,...
type HttpClient (line 97) | pub struct HttpClient {
method new (line 107) | pub fn new(proxy_url: Option<&Url>) -> Self {
method try_create_hyper_client (line 147) | fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result<HyperCli...
method hyper_client (line 176) | fn hyper_client(&self) -> &HyperClient {
method request (line 181) | pub async fn request(&self, req: Request<Bytes>) -> Result<Response<In...
method request_body (line 224) | pub async fn request_body(&self, req: Request<Bytes>) -> Result<Bytes,...
method request_stream (line 229) | pub fn request_stream(&self, req: Request<Bytes>) -> Result<IntoStream...
method request_fut (line 233) | pub fn request_fut(&self, mut req: Request<Bytes>) -> Result<ResponseF...
method get_retry_after (line 259) | pub fn get_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Dur...
FILE: core/src/login5.rs
constant MAX_LOGIN_TRIES (line 23) | const MAX_LOGIN_TRIES: u8 = 3;
constant LOGIN_TIMEOUT (line 24) | const LOGIN_TIMEOUT: Duration = Duration::from_secs(3);
type Login5Error (line 33) | enum Login5Error {
method from (line 47) | fn from(err: Login5Error) -> Self {
method request (line 61) | async fn request(&self, message: &LoginRequest) -> Result<Bytes, Error> {
method login5_request (line 75) | async fn login5_request(&self, login: Login_method) -> Result<LoginOk, E...
method login (line 136) | pub async fn login(
method auth_token (line 166) | pub async fn auth_token(&self) -> Result<Token, Error> {
method handle_challenges (line 207) | fn handle_challenges(
method token_from_login (line 261) | fn token_from_login(token: String, expires_in: i32) -> Token {
FILE: core/src/mercury/mod.rs
type MercuryPending (line 31) | pub struct MercuryPending {
type MercuryFuture (line 37) | pub struct MercuryFuture<T> {
type Output (line 42) | type Output = Result<T, Error>;
method poll (line 44) | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Ou...
method next_seq (line 50) | fn next_seq(&self) -> Vec<u8> {
method request (line 56) | fn request(&self, req: MercuryRequest) -> Result<MercuryFuture<MercuryRe...
method get (line 79) | pub fn get<T: Into<String>>(&self, uri: T) -> Result<MercuryFuture<Mercu...
method send (line 88) | pub fn send<T: Into<String>>(
method sender (line 101) | pub fn sender<T: Into<String>>(&self, uri: T) -> MercurySender {
method subscribe (line 105) | pub fn subscribe<T: Into<String>>(
method listen_for (line 154) | pub fn listen_for<T: Into<String>>(
method dispatch (line 175) | pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Resul...
method parse_part (line 223) | fn parse_part(data: &mut Bytes) -> Vec<u8> {
method complete_request (line 228) | fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending)...
method shutdown (line 298) | pub(crate) fn shutdown(&self) {
FILE: core/src/mercury/sender.rs
type MercurySender (line 7) | pub struct MercurySender {
method new (line 15) | pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySend...
method is_flushed (line 24) | pub fn is_flushed(&self) -> bool {
method send (line 28) | pub fn send(&mut self, item: Vec<u8>) -> Result<(), Error> {
method flush (line 34) | pub async fn flush(&mut self) -> Result<(), Error> {
method clone (line 48) | fn clone(&self) -> MercurySender {
FILE: core/src/mercury/types.rs
type MercuryMethod (line 10) | pub enum MercuryMethod {
method fmt (line 53) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
method command (line 65) | pub fn command(&self) -> PacketType {
type MercuryRequest (line 18) | pub struct MercuryRequest {
method encode (line 76) | pub fn encode(&self, seq: &[u8]) -> Result<Vec<u8>, Error> {
type MercuryResponse (line 26) | pub struct MercuryResponse {
type MercuryError (line 33) | pub enum MercuryError {
method from (line 43) | fn from(err: MercuryError) -> Self {
FILE: core/src/packet.rs
type PacketType (line 6) | pub enum PacketType {
FILE: core/src/proxytunnel.rs
function proxy_connect (line 5) | pub async fn proxy_connect<T: AsyncRead + AsyncWrite + Unpin>(
FILE: core/src/session.rs
constant SESSION_DATA_POISON_MSG (line 46) | const SESSION_DATA_POISON_MSG: &str = "session data rwlock should not be...
type SessionError (line 49) | pub enum SessionError {
method from (line 61) | fn from(err: SessionError) -> Self {
method from (line 72) | fn from(err: quick_xml::encoding::EncodingError) -> Self {
type UserAttributes (line 77) | pub type UserAttributes = HashMap<String, String>;
type UserData (line 80) | pub struct UserData {
type SessionData (line 87) | struct SessionData {
type SessionInternal (line 100) | struct SessionInternal {
type Session (line 130) | pub struct Session(Arc<SessionInternal>);
method new (line 133) | pub fn new(config: SessionConfig, cache: Option<Cache>) -> Self {
method connect_inner (line 163) | async fn connect_inner(
method connect (line 206) | pub async fn connect(
method apresolver (line 290) | pub fn apresolver(&self) -> &ApResolver {
method audio_key (line 296) | pub fn audio_key(&self) -> &AudioKeyManager {
method channel (line 302) | pub fn channel(&self) -> &ChannelManager {
method http_client (line 308) | pub fn http_client(&self) -> &HttpClient {
method mercury (line 312) | pub fn mercury(&self) -> &MercuryManager {
method dealer (line 318) | pub fn dealer(&self) -> &DealerManager {
method spclient (line 324) | pub fn spclient(&self) -> &SpClient {
method token_provider (line 328) | pub fn token_provider(&self) -> &TokenProvider {
method login5 (line 334) | pub fn login5(&self) -> &Login5Manager {
method time_delta (line 340) | pub fn time_delta(&self) -> i64 {
method spawn (line 348) | pub fn spawn<T>(&self, task: T)
method debug_info (line 356) | fn debug_info(&self) {
method check_catalogue (line 364) | fn check_catalogue(attributes: &UserAttributes) {
method send_packet (line 376) | pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) -> Result<()...
method cache (line 383) | pub fn cache(&self) -> Option<&Arc<Cache>> {
method config (line 387) | pub fn config(&self) -> &SessionConfig {
method user_data (line 394) | pub fn user_data(&self) -> UserData {
method session_id (line 403) | pub fn session_id(&self) -> String {
method set_session_id (line 412) | pub fn set_session_id(&self, session_id: &str) {
method device_id (line 423) | pub fn device_id(&self) -> &str {
method client_id (line 427) | pub fn client_id(&self) -> String {
method set_client_id (line 436) | pub fn set_client_id(&self, client_id: &str) {
method client_name (line 447) | pub fn client_name(&self) -> String {
method set_client_name (line 456) | pub fn set_client_name(&self, client_name: &str) {
method client_brand_name (line 467) | pub fn client_brand_name(&self) -> String {
method set_client_brand_name (line 476) | pub fn set_client_brand_name(&self, client_brand_name: &str) {
method client_model_name (line 487) | pub fn client_model_name(&self) -> String {
method set_client_model_name (line 496) | pub fn set_client_model_name(&self, client_model_name: &str) {
method connection_id (line 507) | pub fn connection_id(&self) -> String {
method set_connection_id (line 516) | pub fn set_connection_id(&self, connection_id: &str) {
method username (line 527) | pub fn username(&self) -> String {
method set_username (line 537) | pub fn set_username(&self, username: &str) {
method auth_data (line 549) | pub fn auth_data(&self) -> Vec<u8> {
method set_auth_data (line 558) | pub fn set_auth_data(&self, auth_data: &[u8]) {
method country (line 569) | pub fn country(&self) -> String {
method filter_explicit_content (line 579) | pub fn filter_explicit_content(&self) -> bool {
method autoplay (line 586) | pub fn autoplay(&self) -> bool {
method set_user_attribute (line 597) | pub fn set_user_attribute(&self, key: &str, value: &str) -> Option<Str...
method set_user_attributes (line 611) | pub fn set_user_attributes(&self, attributes: UserAttributes) {
method get_user_attribute (line 623) | pub fn get_user_attribute(&self, key: &str) -> Option<String> {
method weak (line 634) | fn weak(&self) -> SessionWeak {
method shutdown (line 638) | pub fn shutdown(&self) {
method is_invalid (line 645) | pub fn is_invalid(&self) -> bool {
type SessionWeak (line 651) | pub struct SessionWeak(Weak<SessionInternal>);
method try_upgrade (line 654) | fn try_upgrade(&self) -> Option<Session> {
method upgrade (line 658) | pub(crate) fn upgrade(&self) -> Session {
method drop (line 665) | fn drop(&mut self) {
type KeepAliveState (line 671) | enum KeepAliveState {
method debug (line 689) | fn debug(&self, sleep: &Sleep) {
constant INITIAL_PING_TIMEOUT (line 683) | const INITIAL_PING_TIMEOUT: TokioDuration = TokioDuration::from_secs(20);
constant PING_TIMEOUT (line 684) | const PING_TIMEOUT: TokioDuration = TokioDuration::from_secs(80);
constant PONG_DELAY (line 685) | const PONG_DELAY: TokioDuration = TokioDuration::from_secs(60);
constant PONG_ACK_TIMEOUT (line 686) | const PONG_ACK_TIMEOUT: TokioDuration = TokioDuration::from_secs(20);
function new (line 727) | fn new(session: SessionWeak, stream: S) -> Self {
function dispatch (line 736) | fn dispatch(
type Output (line 875) | type Output = Result<(), S::Error>;
method poll (line 877) | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Ou...
FILE: core/src/socket.rs
function connect (line 8) | pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::...
FILE: core/src/spclient.rs
type SpClientResult (line 52) | pub type SpClientResult = Result<Bytes, Error>;
constant CLIENT_TOKEN (line 55) | pub const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-tok...
constant CONNECTION_ID (line 57) | const CONNECTION_ID: HeaderName = HeaderName::from_static("x-spotify-con...
constant NO_METRICS_AND_SALT (line 59) | const NO_METRICS_AND_SALT: RequestOptions = RequestOptions {
type SpClientError (line 66) | pub enum SpClientError {
method from (line 76) | fn from(err: SpClientError) -> Self {
type RequestStrategy (line 82) | pub enum RequestStrategy {
method default (line 88) | fn default() -> Self {
type RequestOptions (line 93) | pub struct RequestOptions {
method default (line 100) | fn default() -> Self {
type TransferRequest (line 110) | pub struct TransferRequest {
method set_strategy (line 115) | pub fn set_strategy(&self, strategy: RequestStrategy) {
method flush_accesspoint (line 119) | pub async fn flush_accesspoint(&self) {
method get_accesspoint (line 123) | pub async fn get_accesspoint(&self) -> Result<SocketAddress, Error> {
method base_url (line 141) | pub async fn base_url(&self) -> Result<String, Error> {
method client_token_request (line 146) | async fn client_token_request<M: Message>(&self, message: &M) -> Result<...
method client_token (line 158) | pub async fn client_token(&self) -> Result<String, Error> {
method request_with_protobuf (line 378) | pub async fn request_with_protobuf<M: Message + MessageFull>(
method request_with_protobuf_and_options (line 395) | pub async fn request_with_protobuf_and_options<M: Message + MessageFull>(
method request_as_json (line 415) | pub async fn request_as_json(
method request (line 429) | pub async fn request(
method request_with_options (line 440) | pub async fn request_with_options(
method put_connect_state_request (line 552) | pub async fn put_connect_state_request(&self, state: &PutStateRequest) -...
method delete_connect_state_request (line 562) | pub async fn delete_connect_state_request(&self) -> SpClientResult {
method put_connect_state_inactive (line 567) | pub async fn put_connect_state_inactive(&self, notify: bool) -> SpClient...
method get_extended_metadata (line 580) | pub async fn get_extended_metadata(
method get_metadata (line 595) | pub async fn get_metadata(&self, kind: ExtensionKind, id: &SpotifyUri) -...
method get_track_metadata (line 625) | pub async fn get_track_metadata(&self, track_uri: &SpotifyUri) -> SpClie...
method get_episode_metadata (line 629) | pub async fn get_episode_metadata(&self, episode_uri: &SpotifyUri) -> Sp...
method get_album_metadata (line 634) | pub async fn get_album_metadata(&self, album_uri: &SpotifyUri) -> SpClie...
method get_artist_metadata (line 638) | pub async fn get_artist_metadata(&self, artist_uri: &SpotifyUri) -> SpCl...
method get_show_metadata (line 643) | pub async fn get_show_metadata(&self, show_uri: &SpotifyUri) -> SpClient...
method get_lyrics (line 647) | pub async fn get_lyrics(&self, track_id: &SpotifyId) -> SpClientResult {
method get_lyrics_for_image (line 654) | pub async fn get_lyrics_for_image(
method get_playlist (line 669) | pub async fn get_playlist(&self, playlist_id: &SpotifyId) -> SpClientRes...
method get_user_profile (line 675) | pub async fn get_user_profile(
method get_user_followers (line 702) | pub async fn get_user_followers(&self, username: &str) -> SpClientResult {
method get_user_following (line 709) | pub async fn get_user_following(&self, username: &str) -> SpClientResult {
method get_radio_for_track (line 716) | pub async fn get_radio_for_track(&self, track_uri: &SpotifyUri) -> SpCli...
method get_apollo_station (line 735) | pub async fn get_apollo_station(
method get_next_page (line 764) | pub async fn get_next_page(&self, next_page_uri: &str) -> SpClientResult {
method get_audio_storage (line 773) | pub async fn get_audio_storage(&self, file_id: &FileId) -> SpClientResult {
method stream_from_cdn (line 781) | pub fn stream_from_cdn<U>(
method request_url (line 805) | pub async fn request_url(&self, url: &str) -> SpClientResult {
method get_audio_preview (line 815) | pub async fn get_audio_preview(&self, preview_id: &FileId) -> SpClientRe...
method get_head_file (line 833) | pub async fn get_head_file(&self, file_id: &FileId) -> SpClientResult {
method get_image (line 845) | pub async fn get_image(&self, image_id: &FileId) -> SpClientResult {
method get_context (line 879) | pub async fn get_context(&self, uri: &str) -> Result<Context, Error> {
method get_autoplay_context (line 899) | pub async fn get_autoplay_context(
method get_rootlist (line 927) | pub async fn get_rootlist(&self, from: usize, length: Option<usize>) -> ...
method transfer (line 941) | pub async fn transfer(
FILE: core/src/spotify_id.rs
type SpotifyId (line 11) | pub struct SpotifyId {
constant SIZE (line 34) | const SIZE: usize = 16;
constant SIZE_BASE62 (line 35) | const SIZE_BASE62: usize = 22;
method from_base16 (line 42) | pub fn from_base16(src: &str) -> SpotifyIdResult {
method from_base62 (line 56) | pub fn from_base62(src: &str) -> SpotifyIdResult {
method from_raw (line 80) | pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
method to_base16 (line 91) | pub fn to_base16(&self) -> String {
method to_base62 (line 100) | pub fn to_base62(&self) -> String {
method to_raw (line 143) | pub fn to_raw(&self) -> [u8; Self::SIZE] {
method fmt (line 149) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method fmt (line 155) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Error (line 161) | type Error = crate::Error;
method try_from (line 162) | fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
type Error (line 168) | type Error = crate::Error;
method try_from (line 169) | fn try_from(src: &str) -> Result<Self, Self::Error> {
type Error (line 175) | type Error = crate::Error;
method try_from (line 176) | fn try_from(src: String) -> Result<Self, Self::Error> {
type Error (line 182) | type Error = crate::Error;
method try_from (line 183) | fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
type Error (line 189) | type Error = crate::Error;
method try_from (line 190) | fn try_from(value: &SpotifyUri) -> Result<Self, Self::Error> {
type SpotifyIdError (line 16) | pub enum SpotifyIdError {
method from (line 24) | fn from(err: SpotifyIdError) -> Self {
type SpotifyIdResult (line 29) | pub type SpotifyIdResult = Result<SpotifyId, Error>;
constant BASE62_DIGITS (line 31) | const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzA...
type ConversionCase (line 209) | struct ConversionCase {
function from_base62 (line 310) | fn from_base62() {
function to_base62 (line 321) | fn to_base62() {
function from_base16 (line 330) | fn from_base16() {
function to_base16 (line 341) | fn to_base16() {
function from_raw (line 350) | fn from_raw() {
FILE: core/src/spotify_uri.rs
constant SPOTIFY_ITEM_TYPE_ALBUM (line 7) | const SPOTIFY_ITEM_TYPE_ALBUM: &str = "album";
constant SPOTIFY_ITEM_TYPE_ARTIST (line 8) | const SPOTIFY_ITEM_TYPE_ARTIST: &str = "artist";
constant SPOTIFY_ITEM_TYPE_EPISODE (line 9) | const SPOTIFY_ITEM_TYPE_EPISODE: &str = "episode";
constant SPOTIFY_ITEM_TYPE_PLAYLIST (line 10) | const SPOTIFY_ITEM_TYPE_PLAYLIST: &str = "playlist";
constant SPOTIFY_ITEM_TYPE_SHOW (line 11) | const SPOTIFY_ITEM_TYPE_SHOW: &str = "show";
constant SPOTIFY_ITEM_TYPE_TRACK (line 12) | const SPOTIFY_ITEM_TYPE_TRACK: &str = "track";
constant SPOTIFY_ITEM_TYPE_LOCAL (line 13) | const SPOTIFY_ITEM_TYPE_LOCAL: &str = "local";
constant SPOTIFY_ITEM_TYPE_UNKNOWN (line 14) | const SPOTIFY_ITEM_TYPE_UNKNOWN: &str = "unknown";
type SpotifyUriError (line 17) | pub enum SpotifyUriError {
method from (line 25) | fn from(err: SpotifyUriError) -> Self {
type SpotifyUriResult (line 30) | pub type SpotifyUriResult = Result<SpotifyUri, Error>;
type SpotifyUri (line 33) | pub enum SpotifyUri {
method is_playable (line 67) | pub fn is_playable(&self) -> bool {
method item_type (line 75) | pub fn item_type(&self) -> &'static str {
method to_id (line 90) | pub fn to_id(&self) -> String {
method from_uri (line 121) | pub fn from_uri(src: &str) -> SpotifyUriResult {
method to_uri (line 206) | pub fn to_uri(&self) -> String {
method to_base62 (line 227) | pub fn to_base62(&self) -> String {
method fmt (line 233) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method fmt (line 239) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Error (line 245) | type Error = crate::Error;
method try_from (line 246) | fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::E...
type Error (line 254) | type Error = crate::Error;
method try_from (line 255) | fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self:...
type Error (line 263) | type Error = crate::Error;
method try_from (line 264) | fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Sel...
type Error (line 272) | type Error = crate::Error;
method try_from (line 273) | fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::E...
type Error (line 281) | type Error = crate::Error;
method try_from (line 282) | fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Err...
type Error (line 290) | type Error = crate::Error;
method try_from (line 291) | fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Sel...
type Error (line 299) | type Error = crate::Error;
method try_from (line 300) | fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self,...
type Error (line 308) | type Error = crate::Error;
method try_from (line 309) | fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<S...
type Error (line 319) | type Error = crate::Error;
method try_from (line 320) | fn try_from(
type Error (line 334) | type Error = crate::Error;
method try_from (line 335) | fn try_from(
type ConversionCase (line 349) | struct ConversionCase {
type ItemTypeCase (line 439) | struct ItemTypeCase {
function to_id (line 485) | fn to_id() {
function item_type (line 492) | fn item_type() {
function from_uri (line 518) | fn from_uri() {
function from_invalid_type_uri (line 531) | fn from_invalid_type_uri() {
function from_local_uri (line 545) | fn from_local_uri() {
function from_local_uri_missing_fields (line 563) | fn from_local_uri_missing_fields() {
function from_named_uri (line 578) | fn from_named_uri() {
function to_uri (line 596) | fn to_uri() {
function to_named_uri (line 603) | fn to_named_uri() {
FILE: core/src/token.rs
type TokenError (line 25) | pub enum TokenError {
method from (line 31) | fn from(err: TokenError) -> Self {
type Token (line 37) | pub struct Token {
constant EXPIRY_THRESHOLD (line 109) | const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
method from_json (line 111) | pub fn from_json(body: String) -> Result<Self, Error> {
method is_expired (line 122) | pub fn is_expired(&self) -> bool {
method in_scope (line 126) | pub fn in_scope(&self, scope: &str) -> bool {
method in_scopes (line 135) | pub fn in_scopes(&self, scopes: Vec<&str>) -> bool {
type TokenData (line 47) | struct TokenData {
method find_token (line 55) | fn find_token(&self, scopes: Vec<&str>) -> Option<usize> {
method get_token (line 65) | pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
method get_token_with_client_id (line 70) | pub async fn get_token_with_client_id(
FILE: core/src/util.rs
function keep_flushing (line 18) | pub(crate) fn keep_flushing<'a, T, S: Sink<T> + Unpin + 'a>(
type CancelOnDrop (line 27) | pub struct CancelOnDrop<T>(pub JoinHandle<T>);
type Output (line 30) | type Output = <JoinHandle<T> as Future>::Output;
method poll (line 32) | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Ou...
method drop (line 38) | fn drop(&mut self) {
type TimeoutOnDrop (line 43) | pub struct TimeoutOnDrop<T: Send + 'static> {
function new (line 49) | pub fn new(handle: JoinHandle<T>, timeout: tokio::time::Duration) -> Self {
function take (line 56) | pub fn take(&mut self) -> Option<JoinHandle<T>> {
type Output (line 62) | type Output = <JoinHandle<T> as Future>::Output;
method poll (line 64) | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Ou...
method drop (line 77) | fn drop(&mut self) {
type Seq (line 101) | pub trait Seq {
method next (line 102) | fn next(&self) -> Self;
type SeqGenerator (line 116) | pub struct SeqGenerator<T: Seq>(T);
function new (line 119) | pub fn new(value: T) -> Self {
function get (line 123) | pub fn get(&mut self) -> T {
function solve_hash_cash (line 129) | pub fn solve_hash_cash(
function get_next_query_separator (line 170) | pub fn get_next_query_separator(url: &str) -> &'static str {
FILE: core/src/version.rs
constant VERSION_STRING (line 2) | pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_GIT_...
constant BUILD_DATE (line 5) | pub const BUILD_DATE: &str = env!("VERGEN_BUILD_DATE");
constant SHA_SHORT (line 8) | pub const SHA_SHORT: &str = env!("VERGEN_GIT_SHA");
constant COMMIT_DATE (line 11) | pub const COMMIT_DATE: &str = env!("VERGEN_GIT_COMMIT_DATE");
constant SEMVER (line 14) | pub const SEMVER: &str = env!("CARGO_PKG_VERSION");
constant BUILD_ID (line 17) | pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID");
constant SPOTIFY_VERSION (line 20) | pub const SPOTIFY_VERSION: u64 = 124200290;
constant SPOTIFY_SEMANTIC_VERSION (line 23) | pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.52.442";
constant SPOTIFY_PROPERTY_SET_ID (line 26) | pub const SPOTIFY_PROPERTY_SET_ID: &str = "b4c7e4b5835079ed94391b2e65fca...
constant SPOTIFY_MOBILE_VERSION (line 29) | pub const SPOTIFY_MOBILE_VERSION: &str = "8.9.82.620";
constant SPOTIFY_MOBILE_PROPERTY_SET_ID (line 32) | pub const SPOTIFY_MOBILE_PROPERTY_SET_ID: &str =
constant SPOTIFY_SPIRC_VERSION (line 36) | pub const SPOTIFY_SPIRC_VERSION: &str = "3.2.6";
constant FALLBACK_USER_AGENT (line 39) | pub const FALLBACK_USER_AGENT: &str = "Spotify/124200290 Linux/0 (libres...
function spotify_version (line 41) | pub fn spotify_version() -> String {
function spotify_semantic_version (line 48) | pub fn spotify_semantic_version() -> String {
FILE: core/tests/connect.rs
function test_connection (line 8) | async fn test_connection() {
FILE: discovery/examples/discovery.rs
function main (line 7) | async fn main() {
FILE: discovery/examples/discovery_group.rs
function main (line 7) | async fn main() {
FILE: discovery/src/avahi.rs
type Server (line 22) | pub trait Server {
method entry_group_new (line 25) | fn entry_group_new(&self);
method get_state (line 28) | fn get_state(&self) -> zbus::Result<i32>;
method state_changed (line 32) | fn state_changed(&self, state: i32, error: &str) -> zbus::Result<()>;
type EntryGroupState (line 42) | pub enum EntryGroupState {
constant SIGNATURE (line 56) | const SIGNATURE: &'static zvariant::Signature = &zvariant::Signature::...
type EntryGroup (line 64) | pub trait EntryGroup {
method add_address (line 66) | fn add_address(
method add_record (line 77) | fn add_record(
method add_service (line 91) | fn add_service(
method add_service_subtype (line 106) | fn add_service_subtype(
method commit (line 118) | fn commit(&self) -> zbus::Result<()>;
method free (line 121) | fn free(&self) -> zbus::Result<()>;
method get_state (line 124) | fn get_state(&self) -> zbus::Result<EntryGroupState>;
method is_empty (line 127) | fn is_empty(&self) -> zbus::Result<bool>;
method reset (line 130) | fn reset(&self) -> zbus::Result<()>;
method update_service_txt (line 134) | fn update_service_txt(
method state_changed (line 147) | fn state_changed(&self, state: EntryGroupState, error: &str) -> zbus::...
FILE: discovery/src/lib.rs
type DiscoveryEvent (line 35) | pub enum DiscoveryEvent {
type ZeroconfCmd (line 41) | enum ZeroconfCmd {
type DnsSdHandle (line 45) | pub struct DnsSdHandle {
method shutdown (line 51) | async fn shutdown(self) {
type DnsSdServiceBuilder (line 66) | pub type DnsSdServiceBuilder = fn(
constant BACKENDS (line 75) | pub const BACKENDS: &[(
function find (line 94) | pub fn find(name: Option<&str>) -> Result<DnsSdServiceBuilder, Error> {
type Discovery (line 119) | pub struct Discovery {
method builder (line 525) | pub fn builder<T: Into<String>>(device_id: T, client_id: T) -> Builder {
method new (line 530) | pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Result<Self...
method shutdown (line 534) | pub async fn shutdown(self) {
type Builder (line 130) | pub struct Builder {
method new (line 432) | pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Self {
method name (line 449) | pub fn name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
method device_type (line 455) | pub fn device_type(mut self, device_type: DeviceType) -> Self {
method is_group (line 461) | pub fn is_group(mut self, is_group: bool) -> Self {
method add_alias (line 467) | pub fn add_alias(
method zeroconf_ip (line 482) | pub fn zeroconf_ip(mut self, zeroconf_ip: Vec<std::net::IpAddr>) -> Se...
method zeroconf_backend (line 488) | pub fn zeroconf_backend(mut self, zeroconf_backend: DnsSdServiceBuilde...
method port (line 495) | pub fn port(mut self, port: u16) -> Self {
method launch (line 504) | pub fn launch(self) -> Result<Discovery, Error> {
type DiscoveryError (line 139) | pub enum DiscoveryError {
method from (line 158) | fn from(error: zbus::Error) -> Self {
method from (line 164) | fn from(err: DiscoveryError) -> Self {
constant DNS_SD_SERVICE_NAME (line 176) | const DNS_SD_SERVICE_NAME: &str = "_spotify-connect._tcp";
constant TXT_RECORD (line 178) | const TXT_RECORD: [&str; 2] = ["VERSION=1.0", "CPath=/"];
function avahi_task (line 181) | async fn avahi_task(
function launch_avahi (line 313) | fn launch_avahi(
function launch_dns_sd (line 349) | fn launch_dns_sd(
function launch_libmdns (line 389) | fn launch_libmdns(
type Item (line 540) | type Item = Credentials;
method poll_next (line 542) | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Opt...
FILE: discovery/src/server.rs
type Aes128Ctr (line 30) | type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
type Params (line 32) | type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
type Alias (line 34) | pub struct Alias {
type Config (line 40) | pub struct Config {
type RequestHandler (line 49) | struct RequestHandler {
method new (line 57) | fn new(config: Config, event_tx: mpsc::UnboundedSender<DiscoveryEvent>...
method active_user (line 66) | fn active_user(&self) -> String {
method handle_get_info (line 75) | fn handle_get_info(&self) -> Response<Full<Bytes>> {
method handle_add_user (line 135) | fn handle_add_user(&self, params: &Params<'_>) -> Result<Response<Full...
method not_found (line 231) | fn not_found(&self) -> Response<Full<Bytes>> {
method handle (line 237) | async fn handle(
type DiscoveryServerCmd (line 268) | pub(crate) enum DiscoveryServerCmd {
type DiscoveryServer (line 272) | pub struct DiscoveryServer {
method new (line 278) | pub fn new(
method shutdown (line 358) | pub async fn shutdown(self) {
FILE: examples/get_token.rs
constant SCOPES (line 5) | const SCOPES: &str =
function main (line 9) | async fn main() {
FILE: examples/play.rs
function main (line 17) | async fn main() {
FILE: examples/play_connect.rs
constant CACHE (line 17) | const CACHE: &str = ".cache";
constant CACHE_FILES (line 18) | const CACHE_FILES: &str = ".cache/files";
function main (line 21) | async fn main() -> Result<(), Error> {
FILE: examples/playlist_tracks.rs
function main (line 12) | async fn main() {
FILE: metadata/src/album.rs
type Album (line 27) | pub struct Album {
method tracks (line 68) | pub fn tracks(&self) -> impl Iterator<Item = &SpotifyUri> {
type Error (line 91) | type Error = librespot_core::Error;
method try_from (line 92) | fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self:...
type Albums (line 51) | pub struct Albums(pub Vec<SpotifyUri>);
type Disc (line 56) | pub struct Disc {
type Error (line 121) | type Error = librespot_core::Error;
method try_from (line 122) | fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> {
type Discs (line 63) | pub struct Discs(pub Vec<Disc>);
type Message (line 75) | type Message = protocol::metadata::Album;
method request (line 77) | async fn request(session: &Session, album_uri: &SpotifyUri) -> RequestRe...
method parse (line 85) | fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
FILE: metadata/src/artist.rs
type Artist (line 31) | pub struct Artist {
method albums_current (line 143) | pub fn albums_current(&self) -> impl Iterator<Item = &SpotifyUri> {
method singles_current (line 150) | pub fn singles_current(&self) -> impl Iterator<Item = &SpotifyUri> {
method compilations_current (line 158) | pub fn compilations_current(&self) -> impl Iterator<Item = &SpotifyUri> {
method appears_on_albums_current (line 165) | pub fn appears_on_albums_current(&self) -> impl Iterator<Item = &Spoti...
type Error (line 188) | type Error = librespot_core::Error;
method try_from (line 189) | fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self...
type Artists (line 53) | pub struct Artists(pub Vec<Artist>);
type ArtistWithRole (line 58) | pub struct ArtistWithRole {
type Error (line 221) | type Error = librespot_core::Error;
method try_from (line 222) | fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, ...
type ArtistsWithRole (line 65) | pub struct ArtistsWithRole(pub Vec<ArtistWithRole>);
type TopTracks (line 70) | pub struct TopTracks {
type Error (line 234) | type Error = librespot_core::Error;
method try_from (line 235) | fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
type CountryTopTracks (line 76) | pub struct CountryTopTracks(pub Vec<TopTracks>);
method for_country (line 126) | pub fn for_country(&self, country: &str) -> Tracks {
type AlbumGroup (line 81) | pub struct AlbumGroup(pub Albums);
type Error (line 246) | type Error = librespot_core::Error;
method try_from (line 247) | fn try_from(album_groups: &AlbumGroupMessage) -> Result<Self, Self::Er...
type AlbumGroups (line 95) | pub struct AlbumGroups(pub Vec<AlbumGroup>);
method current_releases (line 256) | pub fn current_releases(&self) -> impl Iterator<Item = &SpotifyUri> {
type Biography (line 100) | pub struct Biography {
method from (line 264) | fn from(biography: &BiographyMessage) -> Self {
type Biographies (line 107) | pub struct Biographies(pub Vec<Biography>);
type ActivityPeriod (line 112) | pub enum ActivityPeriod {
type Error (line 282) | type Error = librespot_core::Error;
method try_from (line 284) | fn try_from(period: &ActivityPeriodMessage) -> Result<Self, Self::Erro...
type ActivityPeriods (line 121) | pub struct ActivityPeriods(pub Vec<ActivityPeriod>);
type Message (line 172) | type Message = protocol::metadata::Artist;
method request (line 174) | async fn request(session: &Session, artist_uri: &SpotifyUri) -> RequestR...
method parse (line 182) | fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
FILE: metadata/src/audio/file.rs
type AudioFileFormat (line 18) | pub enum AudioFileFormat {
type Error (line 42) | type Error = i32;
method try_from (line 44) | fn try_from(value: i32) -> Result<Self, Self::Error> {
method from (line 56) | fn from(value: Format) -> Self {
type AudioFiles (line 78) | pub struct AudioFiles(pub HashMap<AudioFileFormat, FileId>);
method is_ogg_vorbis (line 83) | pub fn is_ogg_vorbis(format: AudioFileFormat) -> bool {
method is_mp3 (line 92) | pub fn is_mp3(format: AudioFileFormat) -> bool {
method is_flac (line 103) | pub fn is_flac(format: AudioFileFormat) -> bool {
method mime_type (line 107) | pub fn mime_type(format: AudioFileFormat) -> Option<&'static str> {
method from (line 121) | fn from(files: &[AudioFileMessage]) -> Self {
FILE: metadata/src/audio/item.rs
type AudioItemResult (line 18) | pub type AudioItemResult = Result<AudioItem, Error>;
type CoverImage (line 21) | pub struct CoverImage {
type AudioItem (line 29) | pub struct AudioItem {
method get_file (line 71) | pub async fn get_file(session: &Session, uri: SpotifyUri) -> AudioItem...
type UniqueFields (line 44) | pub enum UniqueFields {
function get_covers (line 194) | fn get_covers(covers: Images, image_url: String) -> Vec<CoverImage> {
function allowed_for_user (line 220) | fn allowed_for_user(user_data: &UserData, restrictions: &Restrictions) -...
function available (line 259) | fn available(availability: &Availabilities) -> AudioItemAvailability {
function available_for_user (line 275) | fn available_for_user(
FILE: metadata/src/availability.rs
type AudioItemAvailability (line 15) | pub type AudioItemAvailability = Result<(), UnavailabilityReason>;
type Availability (line 18) | pub struct Availability {
type Error (line 41) | type Error = librespot_core::Error;
method try_from (line 42) | fn try_from(availability: &AvailabilityMessage) -> Result<Self, Self::...
type Availabilities (line 24) | pub struct Availabilities(pub Vec<Availability>);
type UnavailabilityReason (line 29) | pub enum UnavailabilityReason {
FILE: metadata/src/content_rating.rs
type ContentRating (line 12) | pub struct ContentRating {
method from (line 23) | fn from(content_rating: &ContentRatingMessage) -> Self {
type ContentRatings (line 18) | pub struct ContentRatings(pub Vec<ContentRating>);
FILE: metadata/src/copyright.rs
type Copyright (line 13) | pub struct Copyright {
method from (line 24) | fn from(copyright: &CopyrightMessage) -> Self {
type Copyrights (line 19) | pub struct Copyrights(pub Vec<Copyright>);
FILE: metadata/src/episode.rs
type Episode (line 24) | pub struct Episode {
type Error (line 74) | type Error = librespot_core::Error;
method try_from (line 75) | fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Sel...
type Episodes (line 52) | pub struct Episodes(pub Vec<SpotifyUri>);
type Message (line 58) | type Message = protocol::metadata::Episode;
method request (line 60) | async fn request(session: &Session, episode_uri: &SpotifyUri) -> Request...
method parse (line 68) | fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
FILE: metadata/src/error.rs
type MetadataError (line 5) | pub enum MetadataError {
FILE: metadata/src/external_id.rs
type ExternalId (line 12) | pub struct ExternalId {
method from (line 23) | fn from(external_id: &ExternalIdMessage) -> Self {
type ExternalIds (line 18) | pub struct ExternalIds(pub Vec<ExternalId>);
FILE: metadata/src/image.rs
type Image (line 18) | pub struct Image {
method from (line 59) | fn from(image: &ImageMessage) -> Self {
type Images (line 26) | pub struct Images(pub Vec<Image>);
method from (line 29) | fn from(image_group: &ImageGroup) -> Self {
type PictureSize (line 37) | pub struct PictureSize {
method from (line 72) | fn from(size: &PictureSizeMessage) -> Self {
type PictureSizes (line 43) | pub struct PictureSizes(pub Vec<PictureSize>);
type TranscodedPicture (line 48) | pub struct TranscodedPicture {
type Error (line 83) | type Error = librespot_core::Error;
method try_from (line 84) | fn try_from(picture: &TranscodedPictureMessage) -> Result<Self, Self::...
type TranscodedPictures (line 54) | pub struct TranscodedPictures(pub Vec<TranscodedPicture>);
FILE: metadata/src/lib.rs
type Metadata (line 43) | pub trait Metadata: Send + Sized + 'static {
method request (line 47) | async fn request(session: &Session, id: &SpotifyUri) -> RequestResult;
method get (line 50) | async fn get(session: &Session, id: &SpotifyUri) -> Result<Self, Error> {
method parse (line 57) | fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error>;
FILE: metadata/src/lyrics.rs
type Lyrics (line 33) | pub struct Lyrics {
method get (line 6) | pub async fn get(session: &Session, id: &SpotifyId) -> Result<Self, Er...
method get_for_image (line 12) | pub async fn get_for_image(
type Error (line 24) | type Error = Error;
method try_from (line 26) | fn try_from(lyrics: &Bytes) -> Result<Self, Self::Error> {
type Colors (line 41) | pub struct Colors {
type LyricsInner (line 49) | pub struct LyricsInner {
type SyncType (line 64) | pub enum SyncType {
type Line (line 71) | pub struct Line {
FILE: metadata/src/playlist/annotation.rs
type PlaylistAnnotation (line 16) | pub struct PlaylistAnnotation {
method request_for_user (line 53) | async fn request_for_user(
method get_for_user (line 67) | async fn get_for_user(
type Error (line 88) | type Error = librespot_core::Error;
method try_from (line 89) | fn try_from(
type Message (line 26) | type Message = protocol::playlist_annotate3::PlaylistAnnotation;
method request (line 28) | async fn request(session: &Session, playlist_uri: &SpotifyUri) -> Reques...
method parse (line 41) | fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
FILE: metadata/src/playlist/attribute.rs
type PlaylistAttributes (line 26) | pub struct PlaylistAttributes {
type Error (line 98) | type Error = librespot_core::Error;
method try_from (line 99) | fn try_from(attributes: &PlaylistAttributesMessage) -> Result<Self, Se...
type PlaylistAttributeKinds (line 40) | pub struct PlaylistAttributeKinds(pub Vec<PlaylistAttributeKind>);
type PlaylistFormatAttribute (line 47) | pub struct PlaylistFormatAttribute(pub HashMap<String, String>);
method from (line 116) | fn from(attributes: &[PlaylistFormatAttributeMessage]) -> Self {
type PlaylistItemAttributes (line 52) | pub struct PlaylistItemAttributes {
type Error (line 127) | type Error = librespot_core::Error;
method try_from (line 128) | fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result<Self...
type PlaylistItemAttributeKinds (line 62) | pub struct PlaylistItemAttributeKinds(pub Vec<PlaylistItemAttributeKind>);
type PlaylistPartialAttributes (line 69) | pub struct PlaylistPartialAttributes {
type Error (line 140) | type Error = librespot_core::Error;
method try_from (line 141) | fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result<S...
type PlaylistPartialItemAttributes (line 77) | pub struct PlaylistPartialItemAttributes {
type Error (line 156) | type Error = librespot_core::Error;
method try_from (line 157) | fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Resu...
type PlaylistUpdateAttributes (line 85) | pub struct PlaylistUpdateAttributes {
type Error (line 172) | type Error = librespot_core::Error;
method try_from (line 173) | fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result<Self, ...
type PlaylistUpdateItemAttributes (line 91) | pub struct PlaylistUpdateItemAttributes {
type Error (line 182) | type Error = librespot_core::Error;
method try_from (line 183) | fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result<Se...
FILE: metadata/src/playlist/diff.rs
type PlaylistDiff (line 11) | pub struct PlaylistDiff {
type Error (line 18) | type Error = librespot_core::Error;
method try_from (line 19) | fn try_from(diff: &DiffMessage) -> Result<Self, Self::Error> {
FILE: metadata/src/playlist/item.rs
type PlaylistItem (line 21) | pub struct PlaylistItem {
type Error (line 56) | type Error = librespot_core::Error;
method try_from (line 57) | fn try_from(item: &PlaylistItemMessage) -> Result<Self, Self::Error> {
type PlaylistItems (line 27) | pub struct PlaylistItems(pub Vec<PlaylistItem>);
type PlaylistItemList (line 32) | pub struct PlaylistItemList {
type Error (line 68) | type Error = librespot_core::Error;
method try_from (line 69) | fn try_from(list_items: &PlaylistItemsMessage) -> Result<Self, Self::E...
type PlaylistMetaItem (line 40) | pub struct PlaylistMetaItem {
type Error (line 80) | type Error = librespot_core::Error;
method try_from (line 81) | fn try_from(item: &PlaylistMetaItemMessage) -> Result<Self, Self::Erro...
type PlaylistMetaItems (line 51) | pub struct PlaylistMetaItems(pub Vec<PlaylistMetaItem>);
FILE: metadata/src/playlist/list.rs
type Geoblocks (line 22) | pub struct Geoblocks(Vec<Geoblock>);
type Playlist (line 27) | pub struct Playlist {
method tracks (line 70) | pub fn tracks(&self) -> impl ExactSizeIterator<Item = &SpotifyUri> {
method name (line 82) | pub fn name(&self) -> &str {
type Playlists (line 46) | pub struct Playlists(pub Vec<SpotifyId>);
type SelectedListContent (line 51) | pub struct SelectedListContent {
type Error (line 139) | type Error = librespot_core::Error;
method try_from (line 140) | fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self...
type Message (line 89) | type Message = protocol::playlist4_external::SelectedListContent;
method request (line 91) | async fn request(session: &Session, playlist_uri: &SpotifyUri) -> Reques...
method parse (line 102) | fn parse(msg: &Self::Message, uri: &SpotifyUri) -> Result<Self, Error> {
FILE: metadata/src/playlist/operation.rs
type PlaylistOperation (line 22) | pub struct PlaylistOperation {
type Error (line 60) | type Error = librespot_core::Error;
method try_from (line 61) | fn try_from(operation: &PlaylistOperationMessage) -> Result<Self, Self...
type PlaylistOperations (line 32) | pub struct PlaylistOperations(pub Vec<PlaylistOperation>);
type PlaylistOperationAdd (line 37) | pub struct PlaylistOperationAdd {
type Error (line 82) | type Error = librespot_core::Error;
method try_from (line 83) | fn try_from(add: &PlaylistAddMessage) -> Result<Self, Self::Error> {
type PlaylistOperationMove (line 45) | pub struct PlaylistOperationMove {
method from (line 94) | fn from(mov: &PlaylistMoveMessage) -> Self {
type PlaylistOperationRemove (line 52) | pub struct PlaylistOperationRemove {
type Error (line 104) | type Error = librespot_core::Error;
method try_from (line 105) | fn try_from(remove: &PlaylistRemoveMessage) -> Result<Self, Self::Erro...
FILE: metadata/src/playlist/permission.rs
type Capabilities (line 13) | pub struct Capabilities {
method from (line 28) | fn from(playlist: &CapabilitiesMessage) -> Self {
type PermissionLevels (line 23) | pub struct PermissionLevels(pub Vec<PermissionLevel>);
FILE: metadata/src/request.rs
type RequestResult (line 7) | pub type RequestResult = Result<bytes::Bytes, Error>;
type MercuryRequest (line 10) | pub trait MercuryRequest {
method request (line 11) | async fn request(session: &Session, uri: &str) -> RequestResult {
FILE: metadata/src/restriction.rs
type Restriction (line 16) | pub struct Restriction {
method parse_country_codes (line 35) | fn parse_country_codes(country_codes: &str) -> Vec<String> {
method from (line 41) | fn from(restriction: &RestrictionMessage) -> Self {
type Restrictions (line 25) | pub struct Restrictions(pub Vec<Restriction>);
type RestrictionCatalogues (line 30) | pub struct RestrictionCatalogues(pub Vec<RestrictionCatalogue>);
type StrChunks (line 76) | struct StrChunks<'s>(&'s str, usize);
type StrChunksExt (line 78) | trait StrChunksExt {
method chunks (line 79) | fn chunks(&self, size: usize) -> StrChunks<'_>;
method chunks (line 83) | fn chunks(&self, size: usize) -> StrChunks<'_> {
type Item (line 89) | type Item = &'s str;
method next (line 90) | fn next(&mut self) -> Option<&'s str> {
FILE: metadata/src/sale_period.rs
type SalePeriod (line 17) | pub struct SalePeriod {
type Error (line 29) | type Error = librespot_core::Error;
method try_from (line 30) | fn try_from(sale_period: &SalePeriodMessage) -> Result<Self, Self::Err...
type SalePeriods (line 24) | pub struct SalePeriods(pub Vec<SalePeriod>);
FILE: metadata/src/show.rs
type Show (line 15) | pub struct Show {
type Error (line 53) | type Error = librespot_core::Error;
method try_from (line 54) | fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::...
type Message (line 37) | type Message = protocol::metadata::Show;
method request (line 39) | async fn request(session: &Session, show_uri: &SpotifyUri) -> RequestRes...
method parse (line 47) | fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
FILE: metadata/src/track.rs
type Track (line 24) | pub struct Track {
type Error (line 75) | type Error = librespot_core::Error;
method try_from (line 76) | fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self:...
type Tracks (line 53) | pub struct Tracks(pub Vec<SpotifyUri>);
type Message (line 59) | type Message = protocol::metadata::Track;
method request (line 61) | async fn request(session: &Session, track_uri: &SpotifyUri) -> RequestRe...
method parse (line 69) | fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
FILE: metadata/src/video.rs
type VideoFiles (line 14) | pub struct VideoFiles(pub Vec<FileId>);
FILE: oauth/examples/oauth_async.rs
constant SPOTIFY_CLIENT_ID (line 5) | const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
constant SPOTIFY_REDIRECT_URI (line 6) | const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login";
constant RESPONSE (line 8) | const RESPONSE: &str = r#"
function main (line 18) | async fn main() {
FILE: oauth/examples/oauth_sync.rs
constant SPOTIFY_CLIENT_ID (line 5) | const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
constant SPOTIFY_REDIRECT_URI (line 6) | const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login";
constant RESPONSE (line 8) | const RESPONSE: &str = r#"
function main (line 17) | fn main() {
FILE: oauth/src/lib.rs
type OAuthError (line 54) | pub enum OAuthError {
type OAuthToken (line 127) | pub struct OAuthToken {
function get_code (line 141) | fn get_code(redirect_url: &str) -> Result<AuthorizationCode, OAuthError> {
function get_authcode_stdin (line 158) | fn get_authcode_stdin() -> Result<AuthorizationCode, OAuthError> {
function get_authcode_listener (line 170) | fn get_authcode_listener(
function get_socket_address (line 213) | fn get_socket_address(redirect_uri: &str) -> Option<SocketAddr> {
type OAuthClient (line 225) | pub struct OAuthClient {
method set_auth_url (line 237) | fn set_auth_url(&self) -> PkceCodeVerifier {
method build_token (line 258) | fn build_token(
method get_access_token (line 285) | pub fn get_access_token(&self) -> Result<OAuthToken, OAuthError> {
method refresh_token (line 314) | pub fn refresh_token(&self, refresh_token: &str) -> Result<OAuthToken,...
method get_access_token_async (line 327) | pub async fn get_access_token_async(&self) -> Result<OAuthToken, OAuth...
method refresh_token_async (line 349) | pub async fn refresh_token_async(&self, refresh_token: &str) -> Result...
type OAuthClientBuilder (line 364) | pub struct OAuthClientBuilder {
method new (line 376) | pub fn new(client_id: &str, redirect_uri: &str, scopes: Vec<&str>) -> ...
method open_in_browser (line 388) | pub fn open_in_browser(mut self) -> Self {
method with_custom_message (line 395) | pub fn with_custom_message(mut self, message: &str) -> Self {
method build (line 401) | pub fn build(self) -> Result<OAuthClient, OAuthError> {
function get_access_token (line 436) | pub fn get_access_token(
function get_socket_address_none (line 521) | fn get_socket_address_none() {
function get_socket_address_some (line 531) | fn get_socket_address_some() {
FILE: playback/src/audio_backend/alsa.rs
constant MAX_BUFFER (line 12) | const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames;
constant MIN_BUFFER (line 13) | const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames;
constant ZERO_FRAMES (line 14) | const ZERO_FRAMES: Frames = 0;
constant MAX_PERIOD_DIVISOR (line 16) | const MAX_PERIOD_DIVISOR: Frames = 4;
constant MIN_PERIOD_DIVISOR (line 17) | const MIN_PERIOD_DIVISOR: Frames = 10;
type AlsaError (line 20) | enum AlsaError {
method from (line 72) | fn from(e: AlsaError) -> SinkError {
method from (line 85) | fn from(f: AudioFormat) -> Format {
type AlsaSink (line 98) | pub struct AlsaSink {
constant NAME (line 484) | pub const NAME: &'static str = "alsa";
method write_buf (line 486) | fn write_buf(&mut self) -> SinkResult<()> {
function list_compatible_devices (line 105) | fn list_compatible_devices() -> SinkResult<()> {
function open_device (line 165) | fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, ...
method open (line 389) | fn open(device: Option<String>, format: AudioFormat) -> Self {
method start (line 417) | fn start(&mut self) -> SinkResult<()> {
method stop (line 436) | fn stop(&mut self) -> SinkResult<()> {
method write_bytes (line 456) | fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
FILE: playback/src/audio_backend/gstreamer.rs
constant GSTREAMER_ASYNC_ERROR_POISON_MSG (line 13) | const GSTREAMER_ASYNC_ERROR_POISON_MSG: &str = "gstreamer async error mu...
type GstreamerSink (line 21) | pub struct GstreamerSink {
constant NAME (line 215) | pub const NAME: &'static str = "gstreamer";
method open (line 30) | fn open(device: Option<String>, format: AudioFormat) -> Self {
method start (line 145) | fn start(&mut self) -> SinkResult<()> {
method stop (line 160) | fn stop(&mut self) -> SinkResult<()> {
method drop (line 179) | fn drop(&mut self) {
method write_bytes (line 186) | fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
FILE: playback/src/audio_backend/jackaudio.rs
type JackSink (line 11) | pub struct JackSink {
constant NAME (line 86) | pub const NAME: &'static str = "jackaudio";
type JackData (line 18) | pub struct JackData {
method process (line 25) | fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control {
method open (line 41) | fn open(client_name: Option<String>, format: AudioFormat) -> Self {
method write (line 69) | fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> S...
FILE: playback/src/audio_backend/mod.rs
type SinkError (line 7) | pub enum SinkError {
type SinkResult (line 20) | pub type SinkResult<T> = Result<T, SinkError>;
type Open (line 22) | pub trait Open {
method open (line 23) | fn open(_: Option<String>, format: AudioFormat) -> Self;
type Sink (line 26) | pub trait Sink {
method start (line 27) | fn start(&mut self) -> SinkResult<()> {
method stop (line 30) | fn stop(&mut self) -> SinkResult<()> {
method write (line 33) | fn write(&mut self, packet: AudioPacket, converter: &mut Converter) ->...
type SinkBuilder (line 36) | pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>;
type SinkAsBytes (line 38) | pub trait SinkAsBytes {
method write_bytes (line 39) | fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>;
function mk_sink (line 42) | fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: Aud...
constant BACKENDS (line 124) | pub const BACKENDS: &[(&str, SinkBuilder)] = &[
function find (line 145) | pub fn find(name: Option<String>) -> Option<SinkBuilder> {
FILE: playback/src/audio_backend/pipe.rs
type StdoutError (line 12) | enum StdoutError {
method from (line 27) | fn from(e: StdoutError) -> SinkError {
type StdoutSink (line 38) | pub struct StdoutSink {
constant NAME (line 112) | pub const NAME: &'static str = "pipe";
method open (line 45) | fn open(file: Option<String>, format: AudioFormat) -> Self {
method start (line 64) | fn start(&mut self) -> SinkResult<()> {
method stop (line 85) | fn stop(&mut self) -> SinkResult<()> {
method write_bytes (line 100) | fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
FILE: playback/src/audio_backend/portaudio.rs
type PortAudioSink (line 11) | pub enum PortAudioSink<'a> {
function output_devices (line 26) | fn output_devices() -> Box<dyn Iterator<Item = (DeviceIndex, DeviceInfo)...
function list_outputs (line 35) | fn list_outputs() {
function find_output (line 47) | fn find_output(device: &str) -> Option<DeviceIndex> {
method open (line 54) | fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> {
method start (line 98) | fn start(&mut self) -> SinkResult<()> {
method stop (line 127) | fn stop(&mut self) -> SinkResult<()> {
method write (line 143) | fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> S...
method drop (line 179) | fn drop(&mut self) {
constant NAME (line 185) | pub const NAME: &'static str = "portaudio";
FILE: playback/src/audio_backend/pulseaudio.rs
type PulseError (line 12) | enum PulseError {
method from (line 37) | fn from(e: PulseError) -> SinkError {
type PulseAudioSink (line 49) | pub struct PulseAudioSink {
constant NAME (line 151) | pub const NAME: &'static str = "pulseaudio";
method open (line 58) | fn open(device: Option<String>, format: AudioFormat) -> Self {
method start (line 82) | fn start(&mut self) -> SinkResult<()> {
method stop (line 129) | fn stop(&mut self) -> SinkResult<()> {
method write_bytes (line 141) | fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
FILE: playback/src/audio_backend/rodio.rs
function mk_rodio (line 21) | pub fn mk_rodio(device: Option<String>, format: AudioFormat) -> Box<dyn ...
function mk_rodiojack (line 26) | pub fn mk_rodiojack(device: Option<String>, format: AudioFormat) -> Box<...
type RodioError (line 35) | pub enum RodioError {
method from (line 63) | fn from(_: cpal::DefaultStreamConfigError) -> RodioError {
method from (line 69) | fn from(_: cpal::SupportedStreamConfigsError) -> RodioError {
method from (line 51) | fn from(e: RodioError) -> SinkError {
type RodioSink (line 74) | pub struct RodioSink {
constant NAME (line 268) | pub const NAME: &'static str = "rodio";
function list_formats (line 79) | fn list_formats(device: &cpal::Device) {
function list_outputs (line 110) | fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> {
function create_sink (line 145) | fn create_sink(
function open (line 216) | pub fn open(host: cpal::Host, device: Option<String>, format: AudioForma...
method start (line 232) | fn start(&mut self) -> SinkResult<()> {
method stop (line 237) | fn stop(&mut self) -> SinkResult<()> {
method write (line 243) | fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> S...
FILE: playback/src/audio_backend/sdl.rs
type SdlSink (line 10) | pub enum SdlSink {
constant NAME (line 120) | pub const NAME: &'static str = "sdl";
method open (line 17) | fn open(device: Option<String>, format: AudioFormat) -> Self {
method start (line 55) | fn start(&mut self) -> SinkResult<()> {
method stop (line 70) | fn stop(&mut self) -> SinkResult<()> {
method write (line 85) | fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> S...
FILE: playback/src/audio_backend/subprocess.rs
type SubprocessError (line 12) | enum SubprocessError {
method from (line 48) | fn from(e: SubprocessError) -> SinkError {
type SubprocessSink (line 62) | pub struct SubprocessSink {
constant NAME (line 195) | pub const NAME: &'static str = "subprocess";
method try_restart (line 197) | fn try_restart(&mut self, e: SubprocessError, restarted: &mut bool) ->...
method open (line 69) | fn open(shell_command: Option<String>, format: AudioFormat) -> Self {
method start (line 88) | fn start(&mut self) -> SinkResult<()> {
method stop (line 113) | fn stop(&mut self) -> SinkResult<()> {
method write_bytes (line 143) | fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
FILE: playback/src/config.rs
type Bitrate (line 7) | pub enum Bitrate {
type Err (line 15) | type Err = ();
method from_str (line 16) | fn from_str(s: &str) -> Result<Self, Self::Err> {
type AudioFormat (line 27) | pub enum AudioFormat {
method size (line 55) | pub fn size(&self) -> usize {
type Err (line 38) | type Err = ();
method from_str (line 39) | fn from_str(s: &str) -> Result<Self, Self::Err> {
type NormalisationType (line 67) | pub enum NormalisationType {
type Err (line 75) | type Err = ();
method from_str (line 76) | fn from_str(s: &str) -> Result<Self, Self::Err> {
type NormalisationMethod (line 87) | pub enum NormalisationMethod {
type Err (line 94) | type Err = ();
method from_str (line 95) | fn from_str(s: &str) -> Result<Self, Self::Err> {
type PlayerConfig (line 105) | pub struct PlayerConfig {
method default (line 130) | fn default() -> Self {
type VolumeCtrl (line 152) | pub enum VolumeCtrl {
constant MAX_VOLUME (line 173) | pub const MAX_VOLUME: u16 = u16::MAX;
constant DEFAULT_DB_RANGE (line 176) | pub const DEFAULT_DB_RANGE: f64 = 60.0;
method from_str_with_range (line 178) | pub fn from_str_with_range(s: &str, db_range: f64) -> Result<Self, <Se...
type Err (line 160) | type Err = ();
method from_str (line 161) | fn from_str(s: &str) -> Result<Self, Self::Err> {
method default (line 167) | fn default() -> VolumeCtrl {
FILE: playback/src/convert.rs
type i24 (line 7) | pub struct i24([u8; 3]);
method from_s24 (line 9) | fn from_s24(sample: i32) -> Self {
type Converter (line 20) | pub struct Converter {
method new (line 25) | pub fn new(dither_config: Option<DithererBuilder>) -> Self {
constant SHIFT_S16 (line 41) | const SHIFT_S16: u8 = 15;
constant SHIFT_S24 (line 42) | const SHIFT_S24: u8 = 23;
constant SHIFT_S32 (line 43) | const SHIFT_S32: u8 = 31;
constant SHIFT_16_TO_24 (line 47) | const SHIFT_16_TO_24: u8 = Self::SHIFT_S24 - Self::SHIFT_S16;
constant SHIFT_16_TO_32 (line 48) | const SHIFT_16_TO_32: u8 = Self::SHIFT_S32 - Self::SHIFT_S16;
constant SCALE_S24 (line 51) | const SCALE_S24: f64 = (1_u64 << Self::SHIFT_S24) as f64;
method scale (line 63) | pub fn scale(&mut self, sample: f64, shift: u8) -> f64 {
method clamping_scale_s24 (line 85) | pub fn clamping_scale_s24(&mut self, sample: f64) -> f64 {
method f64_to_f32 (line 96) | pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec<f32> {
method f64_to_s32 (line 101) | pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec<i32> {
method f64_to_s24 (line 110) | pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec<i32> {
method f64_to_s24_3 (line 119) | pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec<i24> {
method f64_to_s16 (line 127) | pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec<i16> {
FILE: playback/src/decoder/mod.rs
type DecoderError (line 14) | pub enum DecoderError {
method from (line 89) | fn from(err: symphonia::core::errors::Error) -> Self {
type DecoderResult (line 21) | pub type DecoderResult<T> = Result<T, DecoderError>;
type AudioPacketError (line 24) | pub enum AudioPacketError {
type AudioPacketResult (line 31) | pub type AudioPacketResult<T> = Result<T, AudioPacketError>;
type AudioPacket (line 33) | pub enum AudioPacket {
method samples (line 40) | pub fn samples(&self) -> AudioPacketResult<&[f64]> {
method raw (line 48) | pub fn raw(&self) -> AudioPacketResult<&[u8]> {
method is_empty (line 56) | pub fn is_empty(&self) -> bool {
type AudioPacketPosition (line 65) | pub struct AudioPacketPosition {
type Target (line 71) | type Target = u32;
method deref (line 72) | fn deref(&self) -> &Self::Target {
type AudioDecoder (line 77) | pub trait AudioDecoder {
method seek (line 78) | fn seek(&mut self, position_ms: u32) -> Result<u32, DecoderError>;
method next_packet (line 79) | fn next_packet(&mut self) -> DecoderResult<Option<(AudioPacketPosition...
function from (line 83) | fn from(err: DecoderError) -> Self {
FILE: playback/src/decoder/passthrough_decoder.rs
function get_header (line 17) | fn get_header<T>(code: u8, rdr: &mut PacketReader<T>) -> DecoderResult<V...
type PassthroughDecoder (line 35) | pub struct PassthroughDecoder<R: Read + Seek> {
function new (line 49) | pub fn new(rdr: R, format: AudioFileFormat) -> DecoderResult<Self> {
function position_pcm_to_ms (line 85) | fn position_pcm_to_ms(position_pcm: u64) -> u32 {
method seek (line 91) | fn seek(&mut self, position_ms: u32) -> Result<u32, DecoderError> {
method next_packet (line 139) | fn next_packet(&mut self) -> DecoderResult<Option<(AudioPacketPosition, ...
FILE: playback/src/decoder/symphonia_decoder.rs
type SymphoniaDecoder (line 17) | pub struct SymphoniaDecoder {
method new (line 35) | pub fn new<R>(input: R, hint: Hint) -> DecoderResult<Self>
method normalisation_data (line 95) | pub fn normalisation_data(&mut self) -> Option<NormalisationData> {
method local_file_metadata (line 120) | pub(crate) fn local_file_metadata(&mut self) -> Option<LocalFileMetada...
method ts_to_ms (line 180) | fn ts_to_ms(&self, ts: u64) -> u32 {
type LocalFileMetadata (line 24) | pub(crate) struct LocalFileMetadata {
method seek (line 193) | fn seek(&mut self, position_ms: u32) -> Result<u32, DecoderError> {
method next_packet (line 220) | fn next_packet(&mut self) -> DecoderResult<Option<(AudioPacketPosition, ...
FILE: playback/src/dither.rs
type Ditherer (line 31) | pub trait Ditherer {
method new (line 32) | fn new() -> Self
method name (line 35) | fn name(&self) -> &'static str;
method noise (line 36) | fn noise(&mut self) -> f64;
method new (line 55) | fn new() -> Self {
method name (line 63) | fn name(&self) -> &'static str {
method noise (line 68) | fn noise(&mut self) -> f64 {
method new (line 83) | fn new() -> Self {
method name (line 98) | fn name(&self) -> &'static str {
method noise (line 103) | fn noise(&mut self) -> f64 {
method new (line 120) | fn new() -> Self {
method name (line 131) | fn name(&self) -> &'static str {
method noise (line 136) | fn noise(&mut self) -> f64 {
function fmt (line 40) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
function create_rng (line 45) | fn create_rng() -> SmallRng {
type TriangularDitherer (line 49) | pub struct TriangularDitherer {
constant NAME (line 74) | pub const NAME: &'static str = "tpdf";
type GaussianDitherer (line 77) | pub struct GaussianDitherer {
constant NAME (line 109) | pub const NAME: &'static str = "gpdf";
type HighPassDitherer (line 112) | pub struct HighPassDitherer {
constant NAME (line 146) | pub const NAME: &'static str = "tpdf_hp";
function mk_ditherer (line 149) | pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
type DithererBuilder (line 153) | pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
function find_ditherer (line 155) | pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
FILE: playback/src/lib.rs
constant SAMPLE_RATE (line 18) | pub const SAMPLE_RATE: u32 = 44100;
constant NUM_CHANNELS (line 19) | pub const NUM_CHANNELS: u8 = 2;
constant SAMPLES_PER_SECOND (line 20) | pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE * NUM_CHANNELS as u32;
constant PAGES_PER_MS (line 21) | pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0;
constant MS_PER_PAGE (line 22) | pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64;
FILE: playback/src/local_file.rs
constant SUPPORTED_FILE_EXTENSIONS (line 23) | const SUPPORTED_FILE_EXTENSIONS: &[&str; 4] = &["mp3", "mp4", "m4p", "fl...
type LocalFileLookup (line 26) | pub struct LocalFileLookup(HashMap<SpotifyUri, PathBuf>);
method get (line 29) | pub fn get(&self, uri: &SpotifyUri) -> Option<&Path> {
function create_local_file_lookup (line 34) | pub fn create_local_file_lookup(directories: &[PathBuf]) -> LocalFileLoo...
function visit_dir (line 58) | fn visit_dir(dir: &Path, accumulator: &mut LocalFileLookup) -> io::Resul...
function get_uri_from_file (line 91) | fn get_uri_from_file(audio_path: &Path, file_extension: &str) -> Result<...
FILE: playback/src/mixer/alsamixer.rs
type AlsaMixer (line 17) | pub struct AlsaMixer {
constant NAME (line 295) | pub const NAME: &'static str = "alsa";
method switched_off (line 297) | fn switched_off(&self) -> bool {
method is_some_linear (line 314) | fn is_some_linear(&self) -> bool {
constant SND_CTL_TLV_DB_GAIN_MUTE (line 32) | const SND_CTL_TLV_DB_GAIN_MUTE: MilliBel = MilliBel(-9999999);
constant ZERO_DB (line 33) | const ZERO_DB: MilliBel = MilliBel(0);
type AlsaMixerError (line 36) | enum AlsaMixerError {
method from (line 52) | fn from(value: AlsaMixerError) -> Self {
method open (line 58) | fn open(config: MixerConfig) -> Result<Self, Error> {
method volume (line 196) | fn volume(&self) -> u16 {
method set_volume (line 236) | fn set_volume(&self, volume: u16) {
FILE: playback/src/mixer/mappings.rs
type MappedCtrl (line 4) | pub trait MappedCtrl {
method to_mapped (line 5) | fn to_mapped(&self, volume: u16) -> f64;
method as_unmapped (line 6) | fn as_unmapped(&self, mapped_volume: f64) -> u16;
method db_range (line 8) | fn db_range(&self) -> f64;
method set_db_range (line 9) | fn set_db_range(&mut self, new_db_range: f64);
method range_ok (line 10) | fn range_ok(&self) -> bool;
method to_mapped (line 14) | fn to_mapped(&self, volume: u16) -> f64 {
method as_unmapped (line 49) | fn as_unmapped(&self, mapped_volume: f64) -> u16 {
method db_range (line 74) | fn db_range(&self) -> f64 {
method set_db_range (line 82) | fn set_db_range(&mut self, new_db_range: f64) {
method range_ok (line 91) | fn range_ok(&self) -> bool {
type VolumeMapping (line 96) | pub trait VolumeMapping {
method linear_to_mapped (line 97) | fn linear_to_mapped(unmapped_volume: f64, db_range: f64) -> f64;
method mapped_to_linear (line 98) | fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64;
method linear_to_mapped (line 107) | fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 {
method mapped_to_linear (line 112) | fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 {
method linear_to_mapped (line 140) | fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 {
method mapped_to_linear (line 145) | fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 {
type LogMapping (line 105) | pub struct LogMapping {}
method coefficients (line 119) | fn coefficients(db_range: f64) -> (f64, f64) {
type CubicMapping (line 138) | pub struct CubicMapping {}
method min_norm (line 152) | fn min_norm(db_range: f64) -> f64 {
FILE: playback/src/mixer/mod.rs
type NoOpVolume (line 8) | pub struct NoOpVolume;
type Mixer (line 10) | pub trait Mixer: Send + Sync {
method open (line 11) | fn open(config: MixerConfig) -> Result<Self, Error>
method volume (line 15) | fn volume(&self) -> u16;
method set_volume (line 16) | fn set_volume(&self, volume: u16);
method get_soft_volume (line 18) | fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
type VolumeGetter (line 23) | pub trait VolumeGetter {
method attenuation_factor (line 24) | fn attenuation_factor(&self) -> f64;
method attenuation_factor (line 29) | fn attenuation_factor(&self) -> f64 {
type MixerConfig (line 43) | pub struct MixerConfig {
method default (line 51) | fn default() -> MixerConfig {
type MixerFn (line 61) | pub type MixerFn = fn(MixerConfig) -> Result<Arc<dyn Mixer>, Error>;
function mk_sink (line 63) | fn mk_sink<M: Mixer + 'static>(config: MixerConfig) -> Result<Arc<dyn Mi...
constant MIXERS (line 67) | pub const MIXERS: &[(&str, MixerFn)] = &[
function find (line 73) | pub fn find(name: Option<&str>) -> Option<MixerFn> {
FILE: playback/src/mixer/softmixer.rs
type SoftMixer (line 10) | pub struct SoftMixer {
constant NAME (line 45) | pub const NAME: &'static str = "softvol";
method open (line 18) | fn open(config: MixerConfig) -> Result<Self, Error> {
method volume (line 28) | fn volume(&self) -> u16 {
method set_volume (line 33) | fn set_volume(&self, volume: u16) {
method get_soft_volume (line 39) | fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
type SoftVolume (line 48) | struct SoftVolume(Arc<AtomicU64>);
method attenuation_factor (line 52) | fn attenuation_factor(&self) -> f64 {
FILE: playback/src/player.rs
constant PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS (line 45) | const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
constant DB_VOLTAGE_RATIO (line 46) | pub const DB_VOLTAGE_RATIO: f64 = 20.0;
constant PCM_AT_0DBFS (line 47) | pub const PCM_AT_0DBFS: f64 = 1.0;
constant SPOTIFY_OGG_HEADER_END (line 51) | const SPOTIFY_OGG_HEADER_END: u64 = 0xa7;
constant LOAD_HANDLES_POISON_MSG (line 53) | const LOAD_HANDLES_POISON_MSG: &str = "load handles mutex should not be ...
type PlayerResult (line 55) | pub type PlayerResult = Result<(), Error>;
type Player (line 57) | pub struct Player {
method new (line 456) | pub fn new<F>(
method is_invalid (line 549) | pub fn is_invalid(&self) -> bool {
method command (line 556) | fn command(&self, cmd: PlayerCommand) {
method load (line 564) | pub fn load(&self, track_id: SpotifyUri, start_playing: bool, position...
method preload (line 572) | pub fn preload(&self, track_id: SpotifyUri) {
method play (line 576) | pub fn play(&self) {
method pause (line 580) | pub fn pause(&self) {
method stop (line 584) | pub fn stop(&self) {
method seek (line 588) | pub fn seek(&self, position_ms: u32) {
method set_session (line 592) | pub fn set_session(&self, session: Session) {
method get_player_event_channel (line 596) | pub fn get_player_event_channel(&self) -> PlayerEventChannel {
method await_end_of_track (line 602) | pub async fn await_end_of_track(&self) {
method set_sink_event_callback (line 614) | pub fn set_sink_event_callback(&self, callback: Option<SinkEventCallba...
method emit_volume_changed_event (line 618) | pub fn emit_volume_changed_event(&self, volume: u16) {
method set_auto_normalise_as_album (line 622) | pub fn set_auto_normalise_as_album(&self, setting: bool) {
method emit_filter_explicit_content_changed_event (line 626) | pub fn emit_filter_explicit_content_changed_event(&self, filter: bool) {
method emit_session_connected_event (line 630) | pub fn emit_session_connected_event(&self, connection_id: String, user...
method emit_session_disconnected_event (line 637) | pub fn emit_session_disconnected_event(&self, connection_id: String, u...
method emit_session_client_changed_event (line 644) | pub fn emit_session_client_changed_event(
method emit_shuffle_changed_event (line 659) | pub fn emit_shuffle_changed_event(&self, shuffle: bool) {
method emit_repeat_changed_event (line 663) | pub fn emit_repeat_changed_event(&self, context: bool, track: bool) {
method emit_auto_play_changed_event (line 667) | pub fn emit_auto_play_changed_event(&self, auto_play: bool) {
method emit_set_queue_event (line 671) | pub fn emit_set_queue_event(
type SinkStatus (line 63) | pub enum SinkStatus {
type SinkEventCallback (line 69) | pub type SinkEventCallback = Box<dyn Fn(SinkStatus) + Send>;
type PlayerInternal (line 71) | struct PlayerInternal {
method ensure_sink_running (line 1624) | fn ensure_sink_running(&mut self) {
method ensure_sink_stopped (line 1640) | fn ensure_sink_stopped(&mut self, temporarily: bool) {
method handle_player_stop (line 1673) | fn handle_player_stop(&mut self) {
method handle_play (line 1712) | fn handle_play(&mut self) {
method handle_pause (line 1740) | fn handle_pause(&mut self) {
method handle_packet (line 1770) | fn handle_packet(
method start_playback (line 1898) | fn start_playback(
method handle_command_load (line 1972) | fn handle_command_load(
method handle_command_preload (line 2183) | fn handle_command_preload(&mut self, track_id: SpotifyUri) {
method handle_command_seek (line 2234) | fn handle_command_seek(&mut self, position_ms: u32) -> PlayerResult {
method handle_command (line 2300) | fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult {
method send_event (line 2421) | fn send_event(&mut self, event: PlayerEvent) {
method load_track (line 2426) | fn load_track(
method preload_data_before_playback (line 2464) | fn preload_data_before_playback(&mut self) -> PlayerResult {
type PlayerCommand (line 102) | enum PlayerCommand {
method fmt (line 2508) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type QueueTrack (line 151) | pub struct QueueTrack {
type PlayerEvent (line 157) | pub enum PlayerEvent {
method get_play_request_id (line 274) | pub fn get_play_request_id(&self) -> Option<u64> {
type PlayerEventChannel (line 309) | pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
function db_to_ratio (line 312) | pub fn db_to_ratio(db: f64) -> f64 {
function ratio_to_db (line 317) | pub fn ratio_to_db(ratio: f64) -> f64 {
function duration_to_coefficient (line 321) | pub fn duration_to_coefficient(duration: Duration) -> f64 {
function coefficient_to_duration (line 325) | pub fn coefficient_to_duration(coefficient: f64) -> Duration {
type NormalisationData (line 330) | pub struct NormalisationData {
method parse_from_ogg (line 351) | fn parse_from_ogg<T: Read + Seek>(mut file: T) -> io::Result<Normalisa...
method get_factor (line 383) | fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 {
method default (line 340) | fn default() -> Self {
method drop (line 688) | fn drop(&mut self) {
type PlayerLoadedTrackData (line 699) | struct PlayerLoadedTrackData {
type PlayerPreload (line 710) | enum PlayerPreload {
type Decoder (line 722) | type Decoder = Box<dyn AudioDecoder + Send>;
type PlayerState (line 724) | enum PlayerState {
method is_playing (line 770) | fn is_playing(&self) -> bool {
method is_stopped (line 783) | fn is_stopped(&self) -> bool {
method is_loading (line 789) | fn is_loading(&self) -> bool {
method decoder (line 794) | fn decoder(&mut self) -> Option<&mut Decoder> {
method playing_to_end_of_track (line 811) | fn playing_to_end_of_track(&mut self) {
method paused_to_playing (line 850) | fn paused_to_playing(&mut self) {
method playing_to_paused (line 892) | fn playing_to_paused(&mut self) {
method fmt (line 2602) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type PlayerTrackLoader (line 934) | struct PlayerTrackLoader {
method find_available_alternative (line 941) | async fn find_available_alternative(&self, audio_item: AudioItem) -> O...
method stream_data_rate (line 966) | fn stream_data_rate(&self, format: AudioFileFormat) -> Option<usize> {
method load_track (line 992) | async fn load_track(
method load_remote_track (line 1009) | async fn load_remote_track(
method load_local_track (line 1248) | async fn load_local_track(
type Output (line 1352) | type Output = ();
method poll (line 1354) | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
method drop (line 2488) | fn drop(&mut self) {
type Subfile (line 2647) | struct Subfile<T: Read + Seek> {
function new (line 2654) | pub fn new(mut stream: T, offset: u64, length: u64) -> Result<Subfile<T>...
method read (line 2667) | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
method seek (line 2673) | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
method is_seekable (line 2697) | fn is_seekable(&self) -> bool {
method byte_len (line 2701) | fn byte_len(&self) -> Option<u64> {
FILE: playback/src/symphonia_util.rs
function get_latest_metadata (line 4) | pub fn get_latest_metadata(probe_result: &mut ProbeResult) -> Option<Met...
FILE: protocol/build.rs
function out_dir (line 7) | fn out_dir() -> PathBuf {
function cleanup (line 11) | fn cleanup() {
function compile (line 15) | fn compile() {
function main (line 82) | fn main() {
FILE: protocol/src/impl_trait/context.rs
method hash (line 6) | fn hash<H: Hasher>(&self, state: &mut H) {
method from (line 16) | fn from(value: Vec<String>) -> Self {
method from (line 31) | fn from(tracks: Vec<ContextTrack>) -> Self {
FILE: protocol/src/impl_trait/player.rs
function hashmap_into (line 15) | fn hashmap_into<T: Into<V>, V>(map: HashMap<String, T>) -> HashMap<Strin...
method from (line 20) | fn from(value: ContextPlayerOptions) -> Self {
method from (line 33) | fn from(value: PlayerRestrictions) -> Self {
method from (line 73) | fn from(value: Restrictions) -> Self {
method from (line 115) | fn from(value: PlayerModeRestrictions) -> Self {
method from (line 124) | fn from(value: ModeRestrictions) -> Self {
method from (line 133) | fn from(value: PlayerRestrictionReasons) -> Self {
method from (line 142) | fn from(value: RestrictionReasons) -> Self {
method from (line 151) | fn from(value: PlayOrigin) -> Self {
method from (line 167) | fn from(value: Suppressions) -> Self {
FILE: src/main.rs
function device_id (line 44) | fn device_id(name: &str) -> String {
function usage (line 48) | fn usage(program: &str, opts: &getopts::Options) -> String {
function setup_logging (line 56) | fn setup_logging(quiet: bool, verbose: bool) {
function list_backends (line 88) | fn list_backends() {
type ParseFileSizeError (line 100) | pub enum ParseFileSizeError {
function parse_file_size (line 111) | pub fn parse_file_size(input: &str) -> Result<u64, ParseFileSizeError> {
function get_version_string (line 159) | fn get_version_string() -> String {
type Setup (line 206) | struct Setup {
function get_setup (line 226) | async fn get_setup() -> Setup {
function set_env_var (line 1868) | async fn set_env_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(key: K, value: V) {
function main (line 1883) | async fn main() {
FILE: src/player_event_handler.rs
type EventHandler (line 10) | pub struct EventHandler {
method new (line 15) | pub fn new(mut player_events: PlayerEventChannel, onevent: &str) -> Se...
method drop (line 281) | fn drop(&mut self) {
function run_program_on_sink_events (line 291) | pub fn run_program_on_sink_events(sink_status: SinkStatus, onevent: &str) {
function run_program (line 307) | fn run_program(env_vars: HashMap<&str, String>, onevent: &str) {
Condensed preview — 660 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,584K chars).
[
{
"path": ".devcontainer/Dockerfile",
"chars": 857,
"preview": "# syntax=docker/dockerfile:1\nARG debian_version=slim-bookworm\nARG rust_version=1.85.0\nFROM rust:${rust_version}-${debian"
},
{
"path": ".devcontainer/Dockerfile.alpine",
"chars": 816,
"preview": "# syntax=docker/dockerfile:1\nARG alpine_version=alpine3.20\nARG rust_version=1.85.0\nFROM rust:${rust_version}-${alpine_ve"
},
{
"path": ".devcontainer/devcontainer.json",
"chars": 691,
"preview": "{\n \"name\": \"Librespot Devcontainer\",\n \"dockerFile\": \"Dockerfile.alpine\",\n \"_postCreateCommand_comment\": \"Uncomment 'p"
},
{
"path": ".dockerignore",
"chars": 29,
"preview": "target\ncache\nprotocol/target\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1592,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n### Look for si"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 604,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
},
{
"path": ".github/dependabot.yml",
"chars": 267,
"preview": "version: 2\n\nupdates:\n - package-ecosystem: github-actions\n schedule:\n interval: weekly\n day: saturday\n "
},
{
"path": ".github/example/prepare-release.event",
"chars": 83,
"preview": "{\n \"action\": \"workflow_dispatch\",\n \"inputs\": {\n \"versionBump\": \"minor\"\n }\n}"
},
{
"path": ".github/scripts/bump-versions.sh",
"chars": 2193,
"preview": "#!/usr/bin/env bash\n\n# $fragment: see possible options https://github.com/christian-draeger/increment-semantic-version/t"
},
{
"path": ".github/workflows/build.yml",
"chars": 3071,
"preview": "---\n# Note, this is used in the badge URL!\nname: build\n\n\"on\":\n push:\n branches: [dev, master]\n paths-ignore:\n "
},
{
"path": ".github/workflows/cross-compile.yml",
"chars": 2197,
"preview": "---\nname: cross-compile\n\n\"on\":\n push:\n branches: [dev, master]\n paths-ignore:\n - \"**.md\"\n - \"docs/**\"\n "
},
{
"path": ".github/workflows/prepare-release.yml",
"chars": 2865,
"preview": "---\n# test with\n# act --job prepare-release --eventpath ./.github/example/prepare-release.event\nname: prepare release\non"
},
{
"path": ".github/workflows/quality.yml",
"chars": 2341,
"preview": "---\nname: code-quality\n\n\"on\":\n push:\n branches: [dev, master]\n paths-ignore:\n - \"**.md\"\n - \"docs/**\"\n "
},
{
"path": ".github/workflows/release.yml",
"chars": 840,
"preview": "name: publish on release creation\non:\n release:\n types:\n - created\n workflow_dispatch:\n\njobs:\n publish-crates"
},
{
"path": ".gitignore",
"chars": 87,
"preview": "target\n.cargo\nspotify_appkey.key\n.idea/\n.vagrant/\n.project\n.history\n.cache\n*.save\n*.*~\n"
},
{
"path": ".pre-commit-config.yaml",
"chars": 228,
"preview": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nrepos:\n- repo"
},
{
"path": "CHANGELOG.md",
"chars": 26578,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "COMPILING.md",
"chars": 9305,
"preview": "# Compiling\n\n## Setup\n\nIn order to compile librespot, you will first need to set up a suitable Rust build environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 3817,
"preview": "# Contributing\n\n## Reporting an Issue\n\nIssues are tracked in the Github issue tracker of the librespot repo.\n\nIf you hav"
},
{
"path": "Cargo.toml",
"chars": 8403,
"preview": "[package]\nname = \"librespot\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors.workspace = true\nlicense.workspace "
},
{
"path": "Cross.toml",
"chars": 410,
"preview": "[build]\npre-build = [\n \"dpkg --add-architecture $CROSS_DEB_ARCH\",\n \"apt-get update\",\n \"apt-get --assume-yes ins"
},
{
"path": "LICENSE",
"chars": 1078,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Paul Lietar\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "PUBLISHING.md",
"chars": 2082,
"preview": "# Publishing\n\n## How To\n\n1. [prepare the release](#prepare-the-release)\n2. [create a github-release](#creating-a-github-"
},
{
"path": "README.md",
"chars": 8649,
"preview": "[](https://github.com/librespot-org"
},
{
"path": "SECURITY.md",
"chars": 915,
"preview": "# Security Policy\n\n## Supported Versions\n\nWe will support the latest release and main development branch with security u"
},
{
"path": "audio/Cargo.toml",
"chars": 1056,
"preview": "[package]\nname = \"librespot-audio\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Paul Lietar <paul@lietar."
},
{
"path": "audio/src/decrypt.rs",
"chars": 1424,
"preview": "use std::io;\n\nuse aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};\n\ntype Aes128Ctr = ctr::Ctr128BE<aes::Aes128>"
},
{
"path": "audio/src/fetch/mod.rs",
"chars": 23302,
"preview": "mod receive;\n\nuse std::{\n cmp::min,\n fs,\n io::{self, Read, Seek, SeekFrom},\n sync::{\n Arc, OnceLock,\n"
},
{
"path": "audio/src/fetch/receive.rs",
"chars": 17456,
"preview": "use std::{\n cmp::{max, min},\n io::{Seek, SeekFrom, Write},\n sync::Arc,\n time::{Duration, Instant},\n};\n\nuse b"
},
{
"path": "audio/src/lib.rs",
"chars": 190,
"preview": "#[macro_use]\nextern crate log;\n\nmod decrypt;\nmod fetch;\n\nmod range_set;\n\npub use decrypt::AudioDecrypt;\npub use fetch::{"
},
{
"path": "audio/src/range_set.rs",
"chars": 7884,
"preview": "use std::{\n cmp::{max, min},\n fmt,\n slice::Iter,\n};\n\n#[derive(Copy, Clone, Debug)]\npub struct Range {\n pub s"
},
{
"path": "cache/.gitignore",
"chars": 14,
"preview": "*\n!.gitignore\n"
},
{
"path": "connect/Cargo.toml",
"chars": 1292,
"preview": "[package]\nname = \"librespot-connect\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Paul Lietar <paul@lieta"
},
{
"path": "connect/README.md",
"chars": 2043,
"preview": "[//]: # (This readme is optimized for inline rustdoc, if some links don't work, they will when included in lib.rs)\n\n# Co"
},
{
"path": "connect/src/context_resolver.rs",
"chars": 10785,
"preview": "use crate::{\n core::{Error, Session},\n protocol::{\n autoplay_context_request::AutoplayContextRequest, conte"
},
{
"path": "connect/src/lib.rs",
"chars": 321,
"preview": "#![warn(missing_docs)]\n#![doc=include_str!(\"../README.md\")]\n\n#[macro_use]\nextern crate log;\n\nuse librespot_core as core;"
},
{
"path": "connect/src/model.rs",
"chars": 5448,
"preview": "use crate::{\n core::dealer::protocol::SkipTo, protocol::context_player_options::ContextPlayerOptionOverrides,\n};\n\nuse"
},
{
"path": "connect/src/shuffle_vec.rs",
"chars": 5611,
"preview": "use rand::{Rng, SeedableRng, rngs::SmallRng};\nuse std::{\n ops::{Deref, DerefMut},\n vec::IntoIter,\n};\n\n#[derive(Deb"
},
{
"path": "connect/src/spirc.rs",
"chars": 73069,
"preview": "use crate::{\n LoadContextOptions, LoadRequestOptions, PlayContext,\n context_resolver::{ContextAction, ContextResol"
},
{
"path": "connect/src/state/context.rs",
"chars": 17341,
"preview": "use crate::{\n core::{Error, SpotifyId, SpotifyUri},\n protocol::{\n context::Context,\n context_page::C"
},
{
"path": "connect/src/state/handle.rs",
"chars": 1846,
"preview": "use crate::{\n core::{Error, dealer::protocol::SetQueueCommand},\n state::{\n ConnectState,\n context::{"
},
{
"path": "connect/src/state/metadata.rs",
"chars": 3238,
"preview": "use crate::{\n protocol::{context::Context, context_track::ContextTrack, player::ProvidedTrack},\n state::context::S"
},
{
"path": "connect/src/state/options.rs",
"chars": 3127,
"preview": "use crate::{\n core::Error,\n protocol::player::ContextPlayerOptions,\n state::{\n ConnectState, StateError,"
},
{
"path": "connect/src/state/provider.rs",
"chars": 1773,
"preview": "use librespot_protocol::player::ProvidedTrack;\nuse std::fmt::{Display, Formatter};\n\n// providers used by spotify\nconst P"
},
{
"path": "connect/src/state/restrictions.rs",
"chars": 2431,
"preview": "use crate::state::ConnectState;\nuse crate::state::provider::IsProvider;\nuse librespot_protocol::player::Restrictions;\nus"
},
{
"path": "connect/src/state/tracks.rs",
"chars": 14663,
"preview": "use crate::{\n core::{Error, SpotifyUri},\n protocol::player::ProvidedTrack,\n state::{\n ConnectState, SPOT"
},
{
"path": "connect/src/state/transfer.rs",
"chars": 6843,
"preview": "use crate::{\n core::Error,\n protocol::{player::ProvidedTrack, transfer_state::TransferState},\n state::{\n "
},
{
"path": "connect/src/state.rs",
"chars": 17376,
"preview": "pub(super) mod context;\nmod handle;\nmod metadata;\nmod options;\npub(super) mod provider;\nmod restrictions;\nmod tracks;\nmo"
},
{
"path": "contrib/Dockerfile",
"chars": 2800,
"preview": "# Cross compilation environment for librespot\n# Build the docker image from the root of the project with the following c"
},
{
"path": "contrib/Dockerfile.Rpi",
"chars": 2019,
"preview": "# Create a docker image for the RPI\n# Build the docker image from the root of the project with the following command :\n#"
},
{
"path": "contrib/cross-compile-armv6hf/Dockerfile",
"chars": 1783,
"preview": "# Cross compilation environment for librespot in armv6hf.\n# Build the docker image from the root of the project with the"
},
{
"path": "contrib/cross-compile-armv6hf/docker-build.sh",
"chars": 626,
"preview": "#!/usr/bin/env bash\nset -eux\n\ncargo install --force --locked bindgen-cli\n\nPI1_TOOLS_DIR=/pi/tools/arm-bcm2708/arm-linux-"
},
{
"path": "contrib/docker-build.sh",
"chars": 513,
"preview": "#!/usr/bin/env bash\nset -eux\n\ncargo build --release --no-default-features --features \"alsa-backend with-libmdns native-t"
},
{
"path": "contrib/event_handler_example.py",
"chars": 3238,
"preview": "#!/usr/bin/python3\nimport os\nimport json\nfrom datetime import datetime\n\nplayer_event = os.getenv('PLAYER_EVENT')\n\njson_d"
},
{
"path": "contrib/librespot.service",
"chars": 420,
"preview": "[Unit]\nDescription=Librespot (an open source Spotify client)\nDocumentation=https://github.com/librespot-org/librespot\nDo"
},
{
"path": "contrib/librespot.user.service",
"chars": 375,
"preview": "[Unit]\nDescription=Librespot (an open source Spotify client)\nDocumentation=https://github.com/librespot-org/librespot\nDo"
},
{
"path": "core/Cargo.toml",
"chars": 3705,
"preview": "[package]\nname = \"librespot-core\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Paul Lietar <paul@lietar.n"
},
{
"path": "core/build.rs",
"chars": 953,
"preview": "use rand::Rng;\nuse rand_distr::Alphanumeric;\nuse vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder};\n\nfn main() -> Resu"
},
{
"path": "core/src/apresolve.rs",
"chars": 5215,
"preview": "use std::collections::VecDeque;\n\nuse bytes::Bytes;\nuse hyper::{Method, Request};\nuse serde::Deserialize;\n\nuse crate::Err"
},
{
"path": "core/src/audio_key.rs",
"chars": 3569,
"preview": "use std::{collections::HashMap, io::Write, time::Duration};\n\nuse byteorder::{BigEndian, ByteOrder, WriteBytesExt};\nuse b"
},
{
"path": "core/src/authentication.rs",
"chars": 5576,
"preview": "use std::io::{self, Read};\n\nuse aes::Aes192;\nuse base64::engine::Engine as _;\nuse base64::engine::general_purpose::STAND"
},
{
"path": "core/src/cache.rs",
"chars": 15690,
"preview": "#[cfg(unix)]\nuse std::os::unix::fs::{MetadataExt, OpenOptionsExt};\nuse std::{\n cmp::Reverse,\n collections::HashMap"
},
{
"path": "core/src/cdn_url.rs",
"chars": 9625,
"preview": "use std::ops::{Deref, DerefMut};\n\nuse protobuf::Message;\nuse thiserror::Error;\nuse time::Duration;\nuse url::Url;\n\nuse su"
},
{
"path": "core/src/channel.rs",
"chars": 7098,
"preview": "use std::{\n collections::HashMap,\n fmt,\n pin::Pin,\n task::{Context, Poll},\n time::{Duration, Instant},\n};"
},
{
"path": "core/src/component.rs",
"chars": 1372,
"preview": "pub(crate) const COMPONENT_POISON_MSG: &str = \"component mutex should not be poisoned\";\n\nmacro_rules! component {\n ($"
},
{
"path": "core/src/config.rs",
"chars": 5373,
"preview": "use std::{fmt, path::PathBuf, str::FromStr};\n\nuse librespot_protocol::devices::DeviceType as ProtoDeviceType;\nuse url::U"
},
{
"path": "core/src/connection/codec.rs",
"chars": 3004,
"preview": "use std::io;\n\nuse byteorder::{BigEndian, ByteOrder};\nuse bytes::{BufMut, Bytes, BytesMut};\nuse shannon::Shannon;\nuse thi"
},
{
"path": "core/src/connection/handshake.rs",
"chars": 9874,
"preview": "use std::{env::consts::ARCH, io};\n\nuse byteorder::{BigEndian, ByteOrder, WriteBytesExt};\nuse hmac::{Hmac, Mac};\nuse prot"
},
{
"path": "core/src/connection/mod.rs",
"chars": 6172,
"preview": "mod codec;\nmod handshake;\n\npub use self::{codec::ApCodec, handshake::handshake};\n\nuse std::{io, time::Duration};\n\nuse fu"
},
{
"path": "core/src/date.rs",
"chars": 2278,
"preview": "use std::{fmt::Debug, ops::Deref};\n\nuse time::{\n Date as _Date, OffsetDateTime, PrimitiveDateTime, Time, error::Compo"
},
{
"path": "core/src/dealer/manager.rs",
"chars": 5525,
"preview": "use futures_core::Stream;\nuse futures_util::StreamExt;\nuse std::{pin::Pin, str::FromStr, sync::OnceLock};\nuse thiserror:"
},
{
"path": "core/src/dealer/maps.rs",
"chars": 4179,
"preview": "use std::collections::HashMap;\n\nuse crate::Error;\nuse thiserror::Error;\n\n#[derive(Debug, Error)]\npub enum HandlerMapErro"
},
{
"path": "core/src/dealer/mod.rs",
"chars": 20514,
"preview": "pub mod manager;\nmod maps;\npub mod protocol;\n\nuse std::{\n iter,\n pin::Pin,\n sync::{\n Arc, Mutex,\n "
},
{
"path": "core/src/dealer/protocol/request.rs",
"chars": 6817,
"preview": "use crate::{\n deserialize_with::*,\n protocol::{\n context::Context,\n context_player_options::ContextP"
},
{
"path": "core/src/dealer/protocol.rs",
"chars": 5647,
"preview": "pub mod request;\n\npub use request::*;\n\nuse std::collections::HashMap;\nuse std::io::{Error as IoError, Read};\n\nuse crate:"
},
{
"path": "core/src/deserialize_with.rs",
"chars": 2311,
"preview": "use base64::Engine;\nuse base64::prelude::BASE64_STANDARD;\nuse protobuf::MessageFull;\nuse serde::de::{Error, Unexpected};"
},
{
"path": "core/src/diffie_hellman.rs",
"chars": 2121,
"preview": "use std::sync::LazyLock;\n\nuse num_bigint::BigUint;\nuse num_integer::Integer;\nuse num_traits::{One, Zero};\nuse rand::{Cry"
},
{
"path": "core/src/error.rs",
"chars": 12774,
"preview": "use std::{\n error, fmt,\n num::{ParseIntError, TryFromIntError},\n str::Utf8Error,\n string::FromUtf8Error,\n};\n"
},
{
"path": "core/src/file_id.rs",
"chars": 1595,
"preview": "use std::fmt::{self, Write};\n\nuse librespot_protocol as protocol;\n\nconst RAW_LEN: usize = 20;\n\n#[derive(Copy, Clone, Par"
},
{
"path": "core/src/http_client.rs",
"chars": 12096,
"preview": "use std::{\n sync::OnceLock,\n time::{Duration, Instant},\n};\n\nuse bytes::Bytes;\nuse futures_util::{FutureExt, future"
},
{
"path": "core/src/lib.rs",
"chars": 813,
"preview": "#[macro_use]\nextern crate log;\n\nuse librespot_protocol as protocol;\n\n#[macro_use]\nmod component;\n\npub mod apresolve;\npub"
},
{
"path": "core/src/login5.rs",
"chars": 9443,
"preview": "use crate::config::OS;\nuse crate::spclient::CLIENT_TOKEN;\nuse crate::token::Token;\nuse crate::{Error, SessionConfig, uti"
},
{
"path": "core/src/mercury/mod.rs",
"chars": 10196,
"preview": "use std::{\n collections::HashMap,\n future::Future,\n pin::Pin,\n task::{Context, Poll},\n};\n\nuse byteorder::{Bi"
},
{
"path": "core/src/mercury/sender.rs",
"chars": 1475,
"preview": "use std::collections::VecDeque;\n\nuse super::{MercuryFuture, MercuryManager, MercuryResponse};\n\nuse crate::Error;\n\npub st"
},
{
"path": "core/src/mercury/types.rs",
"chars": 2713,
"preview": "use std::io::Write;\n\nuse byteorder::{BigEndian, WriteBytesExt};\nuse protobuf::Message;\nuse thiserror::Error;\n\nuse crate:"
},
{
"path": "core/src/packet.rs",
"chars": 1071,
"preview": "// Ported from librespot-java. Relicensed under MIT with permission.\n\nuse num_derive::{FromPrimitive, ToPrimitive};\n\n#[d"
},
{
"path": "core/src/proxytunnel.rs",
"chars": 1700,
"preview": "use std::io;\n\nuse tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};\n\npub async fn proxy_connect<T: AsyncR"
},
{
"path": "core/src/session.rs",
"chars": 29236,
"preview": "use std::{\n collections::HashMap,\n future::Future,\n io,\n pin::Pin,\n process::exit,\n sync::{Arc, OnceLo"
},
{
"path": "core/src/socket.rs",
"chars": 525,
"preview": "use std::io;\n\nuse tokio::net::TcpStream;\nuse url::Url;\n\nuse crate::proxytunnel;\n\npub async fn connect(host: &str, port: "
},
{
"path": "core/src/spclient.rs",
"chars": 34514,
"preview": "use std::{\n fmt::Write,\n time::{Duration, SystemTime},\n};\n\nuse crate::config::{OS, os_version};\nuse crate::{\n E"
},
{
"path": "core/src/spotify_id.rs",
"chars": 11015,
"preview": "use std::fmt;\n\nuse thiserror::Error;\n\nuse crate::{Error, SpotifyUri};\n\n// re-export FileId for historic reasons, when it"
},
{
"path": "core/src/spotify_uri.rs",
"chars": 19389,
"preview": "use crate::{Error, SpotifyId};\nuse std::{borrow::Cow, fmt, str::FromStr, time::Duration};\nuse thiserror::Error;\n\nuse lib"
},
{
"path": "core/src/token.rs",
"chars": 4363,
"preview": "// Ported from librespot-java. Relicensed under MIT with permission.\n\n// Known scopes:\n// ugc-image-upload, playlist-r"
},
{
"path": "core/src/util.rs",
"chars": 4301,
"preview": "use crate::Error;\nuse byteorder::{BigEndian, ByteOrder};\nuse futures_core::ready;\nuse futures_util::{FutureExt, Sink, Si"
},
{
"path": "core/src/version.rs",
"chars": 1880,
"preview": "/// Version string of the form \"librespot-\\<sha\\>\"\npub const VERSION_STRING: &str = concat!(\"librespot-\", env!(\"VERGEN_G"
},
{
"path": "core/tests/connect.rs",
"chars": 643,
"preview": "use std::time::Duration;\n\nuse tokio::time::timeout;\n\nuse librespot_core::{authentication::Credentials, config::SessionCo"
},
{
"path": "discovery/Cargo.toml",
"chars": 1839,
"preview": "[package]\nname = \"librespot-discovery\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Paul Lietar <paul@lie"
},
{
"path": "discovery/examples/discovery.rs",
"chars": 599,
"preview": "use futures::StreamExt;\nuse librespot_core::SessionConfig;\nuse librespot_discovery::DeviceType;\nuse sha1::{Digest, Sha1}"
},
{
"path": "discovery/examples/discovery_group.rs",
"chars": 632,
"preview": "use futures::StreamExt;\nuse librespot_core::SessionConfig;\nuse librespot_discovery::DeviceType;\nuse sha1::{Digest, Sha1}"
},
{
"path": "discovery/src/avahi.rs",
"chars": 4379,
"preview": "#![cfg(feature = \"with-avahi\")]\n\n#[allow(unused)]\npub use server::ServerProxy;\n\n#[allow(unused)]\npub use entry_group::{\n"
},
{
"path": "discovery/src/lib.rs",
"chars": 18736,
"preview": "//! Advertises this device to Spotify clients in the local network.\n//!\n//! This device will show up in the list of \"ava"
},
{
"path": "discovery/src/server.rs",
"chars": 12419,
"preview": "use std::{\n borrow::Cow,\n collections::BTreeMap,\n net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener},\n sync:"
},
{
"path": "docs/authentication.md",
"chars": 2542,
"preview": "# Authentication\nOnce the connection is setup, the client can authenticate with the AP. For this, it sends an\n`ClientRes"
},
{
"path": "docs/connection.md",
"chars": 2712,
"preview": "# Connection Setup\n## Access point Connection\nThe first step to connecting to Spotify's servers is finding an Access Poi"
},
{
"path": "docs/dealer.md",
"chars": 3641,
"preview": "# Dealer\n\nWhen talking about the dealer, we are speaking about a websocket that represents the player as\nspotify-connect"
},
{
"path": "examples/README.md",
"chars": 1344,
"preview": "# Examples\n\nThis folder contains examples of how to use the `librespot` library for various purposes.\n\n## How to run the"
},
{
"path": "examples/get_token.rs",
"chars": 1363,
"preview": "use std::env;\n\nuse librespot::core::{authentication::Credentials, config::SessionConfig, session::Session};\n\nconst SCOPE"
},
{
"path": "examples/play.rs",
"chars": 1361,
"preview": "use std::{env, process::exit};\n\nuse librespot::{\n core::{\n SpotifyUri, authentication::Credentials, config::Se"
},
{
"path": "examples/play_connect.rs",
"chars": 2359,
"preview": "use librespot::{\n connect::{ConnectConfig, LoadRequest, LoadRequestOptions, Spirc},\n core::{\n Error, authen"
},
{
"path": "examples/playlist_tracks.rs",
"chars": 1251,
"preview": "use std::{env, process::exit};\n\nuse librespot::{\n core::{\n authentication::Credentials, config::SessionConfig,"
},
{
"path": "metadata/Cargo.toml",
"chars": 978,
"preview": "[package]\nname = \"librespot-metadata\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Paul Lietar <paul@liet"
},
{
"path": "metadata/src/album.rs",
"chars": 3916,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::{\n Metadata,\n artist::Artists,\n availabil"
},
{
"path": "metadata/src/artist.rs",
"chars": 10138,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::{\n Metadata,\n album::Albums,\n availabilit"
},
{
"path": "metadata/src/audio/file.rs",
"chars": 4582,
"preview": "use std::{\n collections::HashMap,\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse librespot_core::FileId;\n\nuse cr"
},
{
"path": "metadata/src/audio/item.rs",
"chars": 8865,
"preview": "use std::{fmt::Debug, path::PathBuf};\n\nuse crate::{\n Metadata,\n artist::ArtistsWithRole,\n availability::{AudioI"
},
{
"path": "metadata/src/audio/mod.rs",
"chars": 115,
"preview": "pub mod file;\npub mod item;\n\npub use file::{AudioFileFormat, AudioFiles};\npub use item::{AudioItem, UniqueFields};\n"
},
{
"path": "metadata/src/availability.rs",
"chars": 1343,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse thiserror::Error;\n\nuse crate::util::{impl_deref_wrapped, "
},
{
"path": "metadata/src/content_rating.rs",
"chars": 780,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::{impl_deref_wrapped, impl_from_repeated};\n\nu"
},
{
"path": "metadata/src/copyright.rs",
"chars": 787,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::{impl_deref_wrapped, impl_from_repeated};\n\nu"
},
{
"path": "metadata/src/episode.rs",
"chars": 3598,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::{\n Metadata,\n audio::file::AudioFiles,\n a"
},
{
"path": "metadata/src/error.rs",
"chars": 396,
"preview": "use std::fmt::Debug;\nuse thiserror::Error;\n\n#[derive(Debug, Error)]\npub enum MetadataError {\n #[error(\"empty response"
},
{
"path": "metadata/src/external_id.rs",
"chars": 796,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::{impl_deref_wrapped, impl_from_repeated};\n\nu"
},
{
"path": "metadata/src/image.rs",
"chars": 2378,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::{impl_deref_wrapped, impl_from_repeated, imp"
},
{
"path": "metadata/src/lib.rs",
"chars": 1306,
"preview": "#[macro_use]\nextern crate log;\n\n#[macro_use]\nextern crate async_trait;\n\nuse protobuf::Message;\n\nuse librespot_core::{Err"
},
{
"path": "metadata/src/lyrics.rs",
"chars": 2058,
"preview": "use bytes::Bytes;\n\nuse librespot_core::{Error, FileId, Session, SpotifyId};\n\nimpl Lyrics {\n pub async fn get(session:"
},
{
"path": "metadata/src/playlist/annotation.rs",
"chars": 3167,
"preview": "use std::fmt::Debug;\n\nuse protobuf::Message;\n\nuse crate::{\n Metadata,\n image::TranscodedPictures,\n request::{Me"
},
{
"path": "metadata/src/playlist/attribute.rs",
"chars": 6982,
"preview": "use std::{\n collections::HashMap,\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::{\n image::PictureSize"
},
{
"path": "metadata/src/playlist/diff.rs",
"chars": 957,
"preview": "use std::fmt::Debug;\n\nuse super::operation::PlaylistOperations;\n\nuse librespot_core::SpotifyId;\n\nuse librespot_protocol "
},
{
"path": "metadata/src/playlist/item.rs",
"chars": 2894,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::{impl_deref_wrapped, impl_try_from_repeated}"
},
{
"path": "metadata/src/playlist/list.rs",
"chars": 6407,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::{\n Metadata,\n request::RequestResult,\n ut"
},
{
"path": "metadata/src/playlist/mod.rs",
"chars": 185,
"preview": "pub mod annotation;\npub mod attribute;\npub mod diff;\npub mod item;\npub mod list;\npub mod operation;\npub mod permission;\n"
},
{
"path": "metadata/src/playlist/operation.rs",
"chars": 3444,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::{\n playlist::{\n attribute::{PlaylistUpda"
},
{
"path": "metadata/src/playlist/permission.rs",
"chars": 1470,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::{impl_deref_wrapped, impl_from_repeated_copy"
},
{
"path": "metadata/src/request.rs",
"chars": 1084,
"preview": "use std::fmt::Write;\n\nuse crate::MetadataError;\n\nuse librespot_core::{Error, Session};\n\npub type RequestResult = Result<"
},
{
"path": "metadata/src/restriction.rs",
"chars": 2887,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::impl_deref_wrapped;\nuse crate::util::{impl_f"
},
{
"path": "metadata/src/sale_period.rs",
"chars": 1012,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::{\n restriction::Restrictions,\n util::{impl_d"
},
{
"path": "metadata/src/show.rs",
"chars": 2713,
"preview": "use std::fmt::Debug;\n\nuse crate::{\n Metadata, RequestResult, availability::Availabilities, copyright::Copyrights,\n "
},
{
"path": "metadata/src/track.rs",
"chars": 3664,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse uuid::Uuid;\n\nuse crate::{\n Album, Metadata, RequestRes"
},
{
"path": "metadata/src/util.rs",
"chars": 1473,
"preview": "macro_rules! impl_from_repeated {\n ($src:ty, $dst:ty) => {\n impl From<&[$src]> for $dst {\n fn from("
},
{
"path": "metadata/src/video.rs",
"chars": 412,
"preview": "use std::{\n fmt::Debug,\n ops::{Deref, DerefMut},\n};\n\nuse crate::util::{impl_deref_wrapped, impl_from_repeated};\n\nu"
},
{
"path": "oauth/Cargo.toml",
"chars": 1338,
"preview": "[package]\nname = \"librespot-oauth\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Nick Steel <nick@nsteel.c"
},
{
"path": "oauth/examples/oauth_async.rs",
"chars": 1849,
"preview": "use std::env;\n\nuse librespot_oauth::OAuthClientBuilder;\n\nconst SPOTIFY_CLIENT_ID: &str = \"65b708073fc0480ea92a077233ca87"
},
{
"path": "oauth/examples/oauth_sync.rs",
"chars": 1804,
"preview": "use std::env;\n\nuse librespot_oauth::OAuthClientBuilder;\n\nconst SPOTIFY_CLIENT_ID: &str = \"65b708073fc0480ea92a077233ca87"
},
{
"path": "oauth/src/lib.rs",
"chars": 20628,
"preview": "#![warn(missing_docs)]\n//! Provides a Spotify access token using the OAuth authorization code flow\n//! with PKCE.\n//!\n//"
},
{
"path": "playback/Cargo.toml",
"chars": 3092,
"preview": "[package]\nname = \"librespot-playback\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Sasha Hilton <sashahil"
},
{
"path": "playback/src/audio_backend/alsa.rs",
"chars": 17240,
"preview": "use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};\nuse crate::config::AudioFormat;\nuse crate::convert::Convert"
},
{
"path": "playback/src/audio_backend/gstreamer.rs",
"chars": 7031,
"preview": "use std::sync::{Arc, Mutex};\n\nuse gstreamer::{\n State,\n event::{FlushStart, FlushStop},\n prelude::*,\n};\n\nuse gs"
},
{
"path": "playback/src/audio_backend/jackaudio.rs",
"chars": 2838,
"preview": "use super::{Open, Sink, SinkError, SinkResult};\nuse crate::NUM_CHANNELS;\nuse crate::config::AudioFormat;\nuse crate::conv"
},
{
"path": "playback/src/audio_backend/mod.rs",
"chars": 4973,
"preview": "use crate::config::AudioFormat;\nuse crate::convert::Converter;\nuse crate::decoder::AudioPacket;\nuse thiserror::Error;\n\n#"
},
{
"path": "playback/src/audio_backend/pipe.rs",
"chars": 2941,
"preview": "use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};\nuse crate::config::AudioFormat;\nuse crate::convert::Convert"
},
{
"path": "playback/src/audio_backend/portaudio.rs",
"chars": 5981,
"preview": "use super::{Open, Sink, SinkError, SinkResult};\nuse crate::config::AudioFormat;\nuse crate::convert::Converter;\nuse crate"
},
{
"path": "playback/src/audio_backend/pulseaudio.rs",
"chars": 4720,
"preview": "use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};\nuse crate::config::AudioFormat;\nuse crate::convert::Convert"
},
{
"path": "playback/src/audio_backend/rodio.rs",
"chars": 8424,
"preview": "use std::process::exit;\nuse std::thread;\nuse std::time::Duration;\n\nuse cpal::traits::{DeviceTrait, HostTrait};\nuse thise"
},
{
"path": "playback/src/audio_backend/sdl.rs",
"chars": 3875,
"preview": "use super::{Open, Sink, SinkError, SinkResult};\nuse crate::config::AudioFormat;\nuse crate::convert::Converter;\nuse crate"
},
{
"path": "playback/src/audio_backend/subprocess.rs",
"chars": 6576,
"preview": "use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};\nuse crate::config::AudioFormat;\nuse crate::convert::Convert"
},
{
"path": "playback/src/config.rs",
"chars": 5319,
"preview": "use std::{mem, path::PathBuf, str::FromStr, time::Duration};\n\npub use crate::dither::{DithererBuilder, TriangularDithere"
},
{
"path": "playback/src/convert.rs",
"chars": 5076,
"preview": "use crate::dither::{Ditherer, DithererBuilder};\nuse zerocopy::{Immutable, IntoBytes};\n\n#[derive(Immutable, IntoBytes, Co"
},
{
"path": "playback/src/decoder/mod.rs",
"chars": 2281,
"preview": "use std::ops::Deref;\n\nuse thiserror::Error;\n\n#[cfg(feature = \"passthrough-decoder\")]\nmod passthrough_decoder;\n#[cfg(feat"
},
{
"path": "playback/src/decoder/passthrough_decoder.rs",
"chars": 7344,
"preview": "// Passthrough decoder for librespot\nuse std::{\n io::{Read, Seek},\n time::{SystemTime, UNIX_EPOCH},\n};\n\n// TODO: m"
},
{
"path": "playback/src/decoder/symphonia_decoder.rs",
"chars": 10762,
"preview": "use std::{io, time::Duration};\n\nuse symphonia::core::{\n audio::SampleBuffer,\n codecs::{Decoder, DecoderOptions},\n "
},
{
"path": "playback/src/dither.rs",
"chars": 4946,
"preview": "use rand::SeedableRng;\nuse rand::rngs::SmallRng;\nuse rand_distr::{Distribution, Normal, Triangular, Uniform};\nuse std::f"
},
{
"path": "playback/src/lib.rs",
"chars": 540,
"preview": "#[macro_use]\nextern crate log;\n\nuse librespot_audio as audio;\nuse librespot_core as core;\nuse librespot_metadata as meta"
},
{
"path": "playback/src/local_file.rs",
"chars": 5201,
"preview": "use crate::symphonia_util;\nuse librespot_core::{Error, SpotifyUri};\nuse std::{\n collections::HashMap,\n fs,\n fs:"
},
{
"path": "playback/src/mixer/alsamixer.rs",
"chars": 12196,
"preview": "use crate::player::{db_to_ratio, ratio_to_db};\n\nuse super::mappings::{LogMapping, MappedCtrl, VolumeMapping};\nuse super:"
},
{
"path": "playback/src/mixer/mappings.rs",
"chars": 5894,
"preview": "use super::VolumeCtrl;\nuse crate::player::db_to_ratio;\n\npub trait MappedCtrl {\n fn to_mapped(&self, volume: u16) -> f"
},
{
"path": "playback/src/mixer/mod.rs",
"chars": 1863,
"preview": "use crate::config::VolumeCtrl;\nuse librespot_core::Error;\nuse std::sync::Arc;\n\npub mod mappings;\nuse self::mappings::Map"
},
{
"path": "playback/src/mixer/softmixer.rs",
"chars": 1500,
"preview": "use super::VolumeGetter;\nuse super::{MappedCtrl, VolumeCtrl};\nuse super::{Mixer, MixerConfig};\nuse librespot_core::Error"
},
{
"path": "playback/src/player.rs",
"chars": 97937,
"preview": "use std::{\n collections::HashMap,\n fmt, fs,\n fs::File,\n future::Future,\n io::{self, Read, Seek, SeekFrom}"
},
{
"path": "playback/src/symphonia_util.rs",
"chars": 601,
"preview": "use symphonia::core::meta::Metadata;\nuse symphonia::core::probe::ProbeResult;\n\npub fn get_latest_metadata(probe_result: "
},
{
"path": "protocol/Cargo.toml",
"chars": 403,
"preview": "[package]\nname = \"librespot-protocol\"\nversion = \"0.8.0\"\nrust-version.workspace = true\nauthors = [\"Paul Liétar <paul@liet"
},
{
"path": "protocol/build.rs",
"chars": 3143,
"preview": "use std::{\n env, fs,\n ops::Deref,\n path::{Path, PathBuf},\n};\n\nfn out_dir() -> PathBuf {\n Path::new(&env::var"
},
{
"path": "protocol/proto/AdContext.proto",
"chars": 561,
"preview": "// Extracted from: Spotify 1.1.73.517 (macOS)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimize_"
},
{
"path": "protocol/proto/AdDecisionEvent.proto",
"chars": 288,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AdError.proto",
"chars": 354,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AdEvent.proto",
"chars": 790,
"preview": "// Extracted from: Spotify 1.1.73.517 (macOS)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimize_"
},
{
"path": "protocol/proto/AdRequestEvent.proto",
"chars": 333,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AdSlotEvent.proto",
"chars": 470,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AmazonWakeUpTime.proto",
"chars": 209,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioDriverError.proto",
"chars": 316,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioDriverInfo.proto",
"chars": 329,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioFileSelection.proto",
"chars": 389,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioOffliningSettingsReport.proto",
"chars": 418,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioRateLimit.proto",
"chars": 494,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioSessionEvent.proto",
"chars": 269,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioSettingsReport.proto",
"chars": 1084,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/AudioStreamingSettingsReport.proto",
"chars": 484,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/BoomboxPlaybackInstrumentation.proto",
"chars": 518,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/BrokenObject.proto",
"chars": 294,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/CacheError.proto",
"chars": 467,
"preview": "// Extracted from: Spotify 1.1.73.517 (macOS)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimize_"
},
{
"path": "protocol/proto/CachePruningReport.proto",
"chars": 916,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/CacheRealmPruningReport.proto",
"chars": 817,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/CacheRealmReport.proto",
"chars": 482,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/CacheReport.proto",
"chars": 1084,
"preview": "// Extracted from: Spotify 1.1.73.517 (macOS)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimize_"
},
{
"path": "protocol/proto/ClientLocale.proto",
"chars": 259,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/ColdStartupSequence.proto",
"chars": 558,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/CollectionLevelDbInfo.proto",
"chars": 402,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/CollectionOfflineControllerEmptyTrackList.proto",
"chars": 319,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/ConfigurationApplied.proto",
"chars": 503,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/ConfigurationFetched.proto",
"chars": 1127,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
},
{
"path": "protocol/proto/ConfigurationFetchedNonAuth.proto",
"chars": 1134,
"preview": "// Extracted from: Spotify 1.1.61.583 (Windows)\n\nsyntax = \"proto2\";\n\npackage spotify.event_sender.proto;\n\noption optimiz"
}
]
// ... and 460 more files (download for full content)
About this extraction
This page contains the full source code of the librespot-org/librespot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 660 files (1.4 MB), approximately 361.1k tokens, and a symbol index with 1697 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.