SpotiFLAC 4.2.0__tar.gz → 4.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spotiflac-4.2.0/SpotiFLAC.egg-info → spotiflac-4.2.2}/PKG-INFO +2 -2
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/search.py +66 -24
- spotiflac-4.2.2/SpotiFLAC/api_mixins/signed_sessions.py +55 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/app.py +9 -5
- spotiflac-4.2.2/SpotiFLAC/core/cross_loop_lock.py +53 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/dns_doh.py +4 -2
- spotiflac-4.2.2/SpotiFLAC/core/extension_enrichment.py +340 -0
- spotiflac-4.2.2/SpotiFLAC/core/extension_metadata.py +805 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_cache.py +5 -6
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/lyrics.py +5 -7
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/metadata_enrichment.py +292 -36
- spotiflac-4.2.2/SpotiFLAC/core/metadata_fallback.py +269 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/output_sink.py +24 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/profiles.py +4 -2
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/progress.py +9 -26
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/provider_stats.py +5 -2
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/session_memory.py +3 -1
- spotiflac-4.2.2/SpotiFLAC/core/signed_session_status.py +438 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/tagger.py +12 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/tracklist.py +7 -4
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/downloader.py +47 -8
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/_bridge.js +170 -5
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/manager.py +9 -16
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/provider.py +17 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/app.js +237 -6
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/index.html +28 -4
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/styles.css +12 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/web-shim.js +2 -1
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/launcher.py +58 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/app.py +50 -9
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/banner.py +1 -0
- spotiflac-4.2.2/SpotiFLAC/tui/clipboard.py +82 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/help_screen.py +1 -0
- spotiflac-4.2.2/SpotiFLAC/tui/signed_sessions_view.py +135 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/spotiflac.tcss +2 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/webapp.py +11 -3
- {spotiflac-4.2.0 → spotiflac-4.2.2/SpotiFLAC.egg-info}/PKG-INFO +2 -2
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC.egg-info/SOURCES.txt +16 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/pyproject.toml +2 -2
- spotiflac-4.2.2/tests/test_bridge_mobile_api.py +268 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_core_logic.py +57 -0
- spotiflac-4.2.2/tests/test_cross_loop_locks.py +110 -0
- spotiflac-4.2.2/tests/test_extension_enrichment.py +560 -0
- spotiflac-4.2.2/tests/test_extension_metadata.py +739 -0
- spotiflac-4.2.2/tests/test_metadata_fallback.py +299 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_search_mixin.py +31 -0
- spotiflac-4.2.2/tests/test_signed_session_status.py +305 -0
- spotiflac-4.2.2/tests/test_tqdm_lock_under_capture.py +60 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_app.py +140 -0
- spotiflac-4.2.2/tests/test_tui_signed_sessions.py +116 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/LICENSE +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/MANIFEST.in +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/README.md +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/__init__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/__main__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/__init__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/covers_lyrics.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/csv_import.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/dedup.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/discovery.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/extension_health.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/failed_tracks.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/local_tagging.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/stats.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/subscriptions.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/trust.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/check_update.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/client.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/__init__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/acoustid_lookup.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/apple_music_metadata.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/apple_ttml.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/atomic_io.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/audio_fingerprint.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/base.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/cache_admin.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/canvas.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/cli_preview.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/code_search.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/console.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/csv_picker.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/csv_source.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/db.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/download_log.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/download_validation.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/errors.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/failed_tracks.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/ffmpeg_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/flac_validation.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/health_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/hires_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/history.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/hooks.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/http.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_finder.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_helper.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_utils.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/job_queue.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/library_dedup.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/library_index_cache.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/library_notify.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/library_upgrade.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/link_resolver.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/local_matcher.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/local_processor.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/local_scanner.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/loop_runner.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/models.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/musicbrainz.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/node_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/notifiers.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/output_lock.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/paths.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/playlist_sync.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/quality.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/recording_guard.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/replaygain.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/report.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/response_cache.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_desktop.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_errors.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_mobile.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_mono.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/solver.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/soundplate.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/spotfetch.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_metadata.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_protobuf.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_totp.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/stats.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/subscription_scheduler.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/subscriptions.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/text_match.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/tidal_metadata.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/transcode.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/url_utils.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/core/web_users.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/__init__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/_fsguard.js +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/_netguard.js +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/catalog.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/directories.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/python_provider.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/registry_config.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/runtime.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/runtime_features.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/sandbox.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/extensions/trust.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/am.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/amzn.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/apple-touch-icon.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/download.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/dzr.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/flacdownloader.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/joox.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/kuwo.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo-192.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo-512.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/lrclib.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/migu.jpeg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musicbrainz_d.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musicbrainz_l.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musixmatch.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/netease.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/next.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/pandora.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/qbz.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songlink_d.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songlink_l.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songstats.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/soundcloud.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/spotify.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/spotubedl.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/tidal_d.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/tidal_l.png +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/xbatchdl.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/youtube.svg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/manifest.json +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/sw.js +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/frontend/toast-system.js +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/py.typed +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/__init__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/bump_cache_bust.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/cloud_config_cli.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/dedup_check_cli.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/ext_dryrun.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/ext_scaffold.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/hires_check_cli.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/jellyfin_lyrics_gap.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/library_dedup_cli.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/library_upgrade_cli.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tools/registry_signing_cli.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/__init__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/branding.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/config_state.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/config_view.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/csv_picker_screen.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/extensions_view.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/health_view.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/queue_view.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/runner.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/search_view.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/session_view.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/tracklist_view.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/tui/window_size.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/webapi/__init__.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/webapi/routes.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC/webapi/schemas.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC.egg-info/dependency_links.txt +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC.egg-info/entry_points.txt +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC.egg-info/requires.txt +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/SpotiFLAC.egg-info/top_level.txt +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/setup.cfg +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_acoustid_lookup.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_app_api_mixins.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_artist_credit_split.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_audio_fingerprint.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_bridge_download_segments.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_bridge_retry_utils.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_bridge_startup_errors.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_cache_admin.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_canvas.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_canvas_sidecar.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_cli_preview.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_cover_fitting.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_cover_url_plumbing.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_credential_storage.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_csv_download.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_csv_picker.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_csv_source.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_db_persistence.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_dedup_mixin.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_directories.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_disc_number_from_album.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_discovery_mixin.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_dns_doh.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_download_queue_reporting.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_download_recording_guard_wiring.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_download_resume.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_download_services.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_ext_scaffold_and_dryrun.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_extension_fsguard.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_extension_install_limits.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_extension_netguard.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_extension_output_containment.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_extension_sandbox.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_failed_tracks_and_resume.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_fake_hires_redownload.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_ffmpeg_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_frontend_new_ui_wiring.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_gui_batch_downloads.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_gui_folder_options.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_gui_hires_verification.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_gui_prefetched_selection.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_hires_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_http_redirects.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_job_queue.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_json_report.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_launcher_tui_flag.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_launcher_web_flags.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_library_dedup.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_library_index_cache.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_library_notify.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_library_upgrade.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_link_resolver_resolve_api.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_link_resolver_songstats.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_local_matcher.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_loop_runner.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_lrc_sidecar.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_lyrics_apple_match.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_lyrics_jellyfin_gap.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_lyrics_provider_order.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_m4a_no_roundtrip.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_mono_browser_cleanup.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_mono_page_fallback.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_musicbrainz_fallback.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_musicbrainz_release_pick.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_musicbrainz_tag_names.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_node_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_node_options_quoting.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_notifiers.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_output_lock.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_output_sink.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_playcount_throttle.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_post_download_command_gate.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_post_download_hooks.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_profiles.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_progress_and_csv_counters.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_progress_cross_loop.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_progress_manager_loops.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_quotas.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_recording_guard.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_registry_signing_cli.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_release_date_hydration.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_replaygain.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_runtime_node_check.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_signed_session_backoff.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_signed_session_errors.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_smoke.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_solver_turnstile_locate.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_solver_virtual_display.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_spotify_metadata_nulls.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_stats.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_stream_url_validation.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_subscription_scheduler.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_subscriptions.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_subscriptions_mixin.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tag_number_parsing.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_title_floor.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_track_identity.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_track_isrc_wiring.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tracklist.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_transcode_ffmpeg_install.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_transcode_lossless.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_trust.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_trust_enforcement.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_trust_mixin.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_config_profile.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_config_state.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_csv_picker.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_extensions.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_fixes.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_health.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_look.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_runner.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_search.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_session.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_tracklist.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_tui_window_size.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_watch.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_web_shim_methods_in_sync.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_web_users.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_webapi_integration.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_webapi_v1.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_webapp_auth.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_webapp_isolation.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_webapp_multiuser.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_webapp_ops.py +0 -0
- {spotiflac-4.2.0 → spotiflac-4.2.2}/tests/test_webapp_pwa.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SpotiFLAC
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
4
4
|
Summary: Fetch Spotify track metadata and retrieve matching lossless audio through configurable Tidal, Qobuz & Amazon Music provider backends.
|
|
5
|
-
Author: BartolomeoRusso9
|
|
5
|
+
Author: BartolomeoRusso9
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/BartolomeoRusso9/SpotiFLAC-Module-Version
|
|
8
8
|
Project-URL: Repository, https://github.com/BartolomeoRusso9/SpotiFLAC-Module-Version
|
|
@@ -42,7 +42,9 @@ def empty_results() -> dict[str, list]:
|
|
|
42
42
|
return {"tracks": [], "albums": [], "artists": [], "playlists": []}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def shape_search_results(
|
|
45
|
+
def shape_search_results(
|
|
46
|
+
results: dict, limit: int = 50, provider: str = "spotify"
|
|
47
|
+
) -> dict[str, list]:
|
|
46
48
|
"""Turns the client's objects into the dicts every frontend reads.
|
|
47
49
|
|
|
48
50
|
`getattr` with a default throughout: a provider that stops returning one
|
|
@@ -72,7 +74,7 @@ def shape_search_results(results: dict, limit: int = 50) -> dict[str, list]:
|
|
|
72
74
|
"is_explicit": getattr(t, "is_explicit", False),
|
|
73
75
|
"explicit": getattr(t, "is_explicit", False),
|
|
74
76
|
"isrc": getattr(t, "isrc", ""),
|
|
75
|
-
"provider":
|
|
77
|
+
"provider": provider,
|
|
76
78
|
},
|
|
77
79
|
)
|
|
78
80
|
|
|
@@ -91,7 +93,7 @@ def shape_search_results(results: dict, limit: int = 50) -> dict[str, list]:
|
|
|
91
93
|
"release_date": a.get("release_date", ""),
|
|
92
94
|
"external_urls": a.get("external_url", ""),
|
|
93
95
|
"external_url": a.get("external_url", ""),
|
|
94
|
-
"provider":
|
|
96
|
+
"provider": provider,
|
|
95
97
|
},
|
|
96
98
|
)
|
|
97
99
|
|
|
@@ -107,7 +109,7 @@ def shape_search_results(results: dict, limit: int = 50) -> dict[str, list]:
|
|
|
107
109
|
"cover": art.get("cover_url", ""),
|
|
108
110
|
"external_urls": art.get("external_url", ""),
|
|
109
111
|
"external_url": art.get("external_url", ""),
|
|
110
|
-
"provider":
|
|
112
|
+
"provider": provider,
|
|
111
113
|
},
|
|
112
114
|
)
|
|
113
115
|
|
|
@@ -124,13 +126,31 @@ def shape_search_results(results: dict, limit: int = 50) -> dict[str, list]:
|
|
|
124
126
|
"cover": p.get("cover_url", ""),
|
|
125
127
|
"external_urls": p.get("external_url", ""),
|
|
126
128
|
"external_url": p.get("external_url", ""),
|
|
127
|
-
"provider":
|
|
129
|
+
"provider": provider,
|
|
128
130
|
},
|
|
129
131
|
)
|
|
130
132
|
return out
|
|
131
133
|
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
def _is_spotify(source: str | None) -> bool:
|
|
136
|
+
return not source or source == "spotify"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _search_client(source: str | None):
|
|
140
|
+
"""Spotify, or the catalogue extension named `source` (see
|
|
141
|
+
core/extension_metadata.py — Melon, Bugs, Genie)."""
|
|
142
|
+
if _is_spotify(source):
|
|
143
|
+
from ..core.spotify_metadata import SpotifyMetadataClient
|
|
144
|
+
|
|
145
|
+
return SpotifyMetadataClient()
|
|
146
|
+
from ..core.extension_metadata import ExtensionMetadataClient
|
|
147
|
+
|
|
148
|
+
return ExtensionMetadataClient.for_source(source)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def search_metadata_async(
|
|
152
|
+
query: str, limit: int = 50, source: str | None = None
|
|
153
|
+
) -> dict[str, list]:
|
|
134
154
|
"""Searches and shapes, on the caller's event loop.
|
|
135
155
|
|
|
136
156
|
The client has an async search; the sync `search()` next to it only
|
|
@@ -140,51 +160,73 @@ async def search_metadata_async(query: str, limit: int = 50) -> dict[str, list]:
|
|
|
140
160
|
if not query:
|
|
141
161
|
return empty_results()
|
|
142
162
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
client = SpotifyMetadataClient()
|
|
163
|
+
client = _search_client(source)
|
|
146
164
|
results = await client.search_async(query, limit=limit)
|
|
147
|
-
return shape_search_results(results, limit)
|
|
165
|
+
return shape_search_results(results, limit, source or "spotify")
|
|
148
166
|
|
|
149
167
|
|
|
150
168
|
class SearchMixin:
|
|
151
169
|
"""The two blocking entry points the desktop GUI needs."""
|
|
152
170
|
|
|
153
|
-
def
|
|
171
|
+
def get_metadata_sources(self):
|
|
172
|
+
"""What the search box can search: Spotify, then every installed
|
|
173
|
+
catalogue extension."""
|
|
174
|
+
try:
|
|
175
|
+
from ..core.extension_metadata import metadata_sources
|
|
176
|
+
|
|
177
|
+
return metadata_sources()
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
self.log(f"get_metadata_sources error: {exc}", "error")
|
|
180
|
+
return [{"id": "spotify", "label": "Spotify"}]
|
|
181
|
+
|
|
182
|
+
def search_provider(self, query, limit=50, source=None):
|
|
154
183
|
"""Search music providers (Spotify) for metadata matching `query`.
|
|
155
184
|
|
|
156
185
|
Returns a dictionary with 4 sections: tracks, albums, artists,
|
|
157
186
|
playlists (max `limit` results each).
|
|
158
187
|
"""
|
|
159
188
|
try:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
189
|
+
client = _search_client(source)
|
|
190
|
+
return shape_search_results(
|
|
191
|
+
client.search(query, limit=limit), limit, source or "spotify"
|
|
192
|
+
)
|
|
164
193
|
except Exception as exc:
|
|
165
194
|
self.log(f"search_provider error: {exc}", "error")
|
|
166
195
|
return empty_results()
|
|
167
196
|
|
|
168
|
-
def search_provider_async(self, query, limit=50):
|
|
169
|
-
"""Starts the search in a thread and pushes the result to the window.
|
|
197
|
+
def search_provider_async(self, query, limit=50, source=None, request_id=None):
|
|
198
|
+
"""Starts the search in a thread and pushes the result to the window.
|
|
199
|
+
|
|
200
|
+
`request_id` comes back with the result or the error, so the window
|
|
201
|
+
can drop an answer that arrives after a newer search — typing on, or
|
|
202
|
+
switching the source, starts another while this one is still out.
|
|
203
|
+
"""
|
|
170
204
|
if not query:
|
|
171
205
|
return {"status": "empty"}
|
|
172
206
|
threading.Thread(
|
|
173
207
|
target=self._search_provider_thread,
|
|
174
|
-
args=(query, limit),
|
|
208
|
+
args=(query, limit, source, request_id),
|
|
175
209
|
daemon=True,
|
|
176
210
|
).start()
|
|
177
211
|
return {"status": "started"}
|
|
178
212
|
|
|
179
|
-
def _search_provider_thread(
|
|
213
|
+
def _search_provider_thread(
|
|
214
|
+
self, query, limit, source=None, request_id=None
|
|
215
|
+
) -> None:
|
|
180
216
|
try:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
217
|
+
client = _search_client(source)
|
|
218
|
+
out = shape_search_results(
|
|
219
|
+
client.search(query, limit=limit), limit, source or "spotify"
|
|
220
|
+
)
|
|
185
221
|
except Exception as exc:
|
|
186
|
-
|
|
222
|
+
# A bare string without an id, as before, for callers that send none.
|
|
223
|
+
error: Any = str(exc)
|
|
224
|
+
if request_id is not None:
|
|
225
|
+
error = {"message": str(exc), "request_id": request_id}
|
|
226
|
+
self._push_quietly("app_handle_provider_search_error", error)
|
|
187
227
|
return
|
|
228
|
+
if request_id is not None:
|
|
229
|
+
out["request_id"] = request_id
|
|
188
230
|
self._push_quietly("app_handle_provider_search_results", out)
|
|
189
231
|
|
|
190
232
|
def _push_quietly(self, event: str, payload: Any) -> None:
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""api_mixins/signed_sessions.py — which signed sessions are live, and until when.
|
|
2
|
+
|
|
3
|
+
A thin wrapper over `core/signed_session_status.py`, which is also what the
|
|
4
|
+
terminal UI and `--signed-sessions` call, so the three views cannot disagree.
|
|
5
|
+
|
|
6
|
+
Reading is harmless: no row carries a secret. Clearing and pruning edit
|
|
7
|
+
instance-wide state every account's downloads depend on, so in multi-user
|
|
8
|
+
mode (`owner` set) they are refused to anyone who is not an admin.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SignedSessionsMixin:
|
|
15
|
+
def _may_edit_signed_sessions(self) -> bool:
|
|
16
|
+
owner = getattr(self, "owner", "") or ""
|
|
17
|
+
if not owner:
|
|
18
|
+
return True
|
|
19
|
+
try:
|
|
20
|
+
from ..core.web_users import is_admin
|
|
21
|
+
|
|
22
|
+
return is_admin(owner)
|
|
23
|
+
except Exception:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def get_signed_sessions(self) -> dict:
|
|
27
|
+
"""Shape: {"sessions": [row, ...]} — see `list_signed_sessions`."""
|
|
28
|
+
try:
|
|
29
|
+
from ..core.signed_session_status import list_signed_sessions
|
|
30
|
+
|
|
31
|
+
return {"sessions": list_signed_sessions()}
|
|
32
|
+
except Exception as e:
|
|
33
|
+
return {"sessions": [], "error": str(e)}
|
|
34
|
+
|
|
35
|
+
def clear_signed_session(self, key: str) -> dict:
|
|
36
|
+
if not self._may_edit_signed_sessions():
|
|
37
|
+
return {"ok": False, "error": "Only an admin can clear signed sessions."}
|
|
38
|
+
try:
|
|
39
|
+
from ..core.signed_session_status import clear_signed_session
|
|
40
|
+
|
|
41
|
+
if not clear_signed_session(str(key or "")):
|
|
42
|
+
return {"ok": False, "error": "Unknown session."}
|
|
43
|
+
return {"ok": True}
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return {"ok": False, "error": str(e)}
|
|
46
|
+
|
|
47
|
+
def prune_signed_sessions(self) -> dict:
|
|
48
|
+
if not self._may_edit_signed_sessions():
|
|
49
|
+
return {"ok": False, "error": "Only an admin can remove signed sessions."}
|
|
50
|
+
try:
|
|
51
|
+
from ..core.signed_session_status import prune_orphaned_sessions
|
|
52
|
+
|
|
53
|
+
return {"ok": True, "removed": prune_orphaned_sessions()}
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return {"ok": False, "error": str(e)}
|
|
@@ -24,6 +24,7 @@ from .api_mixins.extension_health import ExtensionHealthMixin
|
|
|
24
24
|
from .api_mixins.failed_tracks import FailedTracksMixin
|
|
25
25
|
from .api_mixins.local_tagging import LocalTaggingMixin
|
|
26
26
|
from .api_mixins.search import SearchMixin
|
|
27
|
+
from .api_mixins.signed_sessions import SignedSessionsMixin
|
|
27
28
|
from .api_mixins.stats import StatsMixin
|
|
28
29
|
from .api_mixins.subscriptions import SubscriptionsMixin
|
|
29
30
|
from .api_mixins.trust import TrustMixin
|
|
@@ -176,6 +177,7 @@ class SpotiFLAC_API(
|
|
|
176
177
|
CsvImportMixin,
|
|
177
178
|
StatsMixin,
|
|
178
179
|
FailedTracksMixin,
|
|
180
|
+
SignedSessionsMixin,
|
|
179
181
|
):
|
|
180
182
|
"""pywebview/`--web` bridge — every method here (plus the two mixins
|
|
181
183
|
above) becomes a callable the frontend invokes as `pywebview.api.<name>`
|
|
@@ -1047,19 +1049,21 @@ class SpotiFLAC_API(
|
|
|
1047
1049
|
# setup — for every search typed into the box.
|
|
1048
1050
|
from .core.tracklist import metadata_client_for
|
|
1049
1051
|
|
|
1050
|
-
client = metadata_client_for(url)
|
|
1051
|
-
|
|
1052
1052
|
# ── Universal call ──────────────────────────────────────────────────
|
|
1053
1053
|
# get_url() is sync on SpotifyMetadataClient but async on
|
|
1054
1054
|
# TidalMetadataClient/AppleMusicMetadataClient — calling it directly
|
|
1055
1055
|
# here would hand back an un-awaited coroutine for the latter two
|
|
1056
1056
|
# (crashing on the very next line, `result[0]`). Reuse the same
|
|
1057
1057
|
# sync/async dispatch helper the real download path already relies
|
|
1058
|
-
# on instead of duplicating (and re-diverging from) that logic
|
|
1058
|
+
# on instead of duplicating (and re-diverging from) that logic —
|
|
1059
|
+
# through the fallback that asks an installed metadata extension
|
|
1060
|
+
# when the built-in Spotify or Apple Music client fails.
|
|
1059
1061
|
# Returns (name, tracks) OR (name, tracks, cover) OR (name, tracks, cover, meta).
|
|
1060
|
-
from .
|
|
1062
|
+
from .core.metadata_fallback import get_url_with_fallback
|
|
1061
1063
|
|
|
1062
|
-
result = await
|
|
1064
|
+
result = await get_url_with_fallback(
|
|
1065
|
+
stripped, lambda: metadata_client_for(url)
|
|
1066
|
+
)
|
|
1063
1067
|
collection_name = result[0]
|
|
1064
1068
|
tracks = result[1]
|
|
1065
1069
|
collection_cover = result[2] if len(result) > 2 else ""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""SpotiFLAC/core/cross_loop_lock.py — a lock `async with` can use from any loop.
|
|
2
|
+
|
|
3
|
+
An ``asyncio.Lock`` binds to the first event loop that has to *wait* on it
|
|
4
|
+
and raises on every other one:
|
|
5
|
+
|
|
6
|
+
RuntimeError: <asyncio.locks.Lock ...> is bound to a different event loop
|
|
7
|
+
|
|
8
|
+
This process runs many loops. ``core/loop_runner.py`` owns a long-lived one
|
|
9
|
+
for the GUI and web bridges, ``client.SpotiFLAC()`` keeps its own
|
|
10
|
+
``asyncio.run()`` per batch, and under ``--web`` each background download is
|
|
11
|
+
its own ``asyncio.run()`` on its own thread. A module-level ``asyncio.Lock``
|
|
12
|
+
therefore works for the first batch and crashes the first time two later ones
|
|
13
|
+
contend. Uncontended acquires never touch the loop, which is why it only
|
|
14
|
+
shows up under load.
|
|
15
|
+
|
|
16
|
+
A ``threading.Lock`` has no loop affinity. It is taken without blocking, and
|
|
17
|
+
the caller yields while it is busy, so the loop is never held up. It also
|
|
18
|
+
serialises *threads*, which the asyncio lock never did: two downloads on two
|
|
19
|
+
threads writing the same cache file were not actually excluded by it.
|
|
20
|
+
|
|
21
|
+
Holding it across an ``await`` is allowed — waiters just poll a little longer
|
|
22
|
+
— but keep what it guards short.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
import threading
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CrossLoopLock:
|
|
32
|
+
"""``async with`` mutual exclusion across event loops and threads."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, poll_interval: float = 0.01) -> None:
|
|
35
|
+
self._lock = threading.Lock()
|
|
36
|
+
self._poll_interval = poll_interval
|
|
37
|
+
|
|
38
|
+
def locked(self) -> bool:
|
|
39
|
+
return self._lock.locked()
|
|
40
|
+
|
|
41
|
+
async def __aenter__(self) -> CrossLoopLock:
|
|
42
|
+
# A plain yield first: an uncontended lock is the overwhelmingly
|
|
43
|
+
# common case, and a contended one is usually free again within a
|
|
44
|
+
# single tick, so the poll interval should be the exception.
|
|
45
|
+
if not self._lock.acquire(blocking=False):
|
|
46
|
+
await asyncio.sleep(0)
|
|
47
|
+
while not self._lock.acquire(blocking=False):
|
|
48
|
+
await asyncio.sleep(self._poll_interval)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, exc_type, exc, tb) -> bool:
|
|
52
|
+
self._lock.release()
|
|
53
|
+
return False
|
|
@@ -26,11 +26,12 @@ its DNS server would be useless in exactly the situation it exists for.
|
|
|
26
26
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
|
-
import asyncio
|
|
30
29
|
import ipaddress
|
|
31
30
|
import logging
|
|
32
31
|
import time
|
|
33
32
|
|
|
33
|
+
from .cross_loop_lock import CrossLoopLock
|
|
34
|
+
|
|
34
35
|
logger = logging.getLogger(__name__)
|
|
35
36
|
|
|
36
37
|
#: Resolvers, by literal address so this never needs DNS itself. Cloudflare
|
|
@@ -47,7 +48,8 @@ DOH_ENDPOINTS = (
|
|
|
47
48
|
_TTL_S = 300.0
|
|
48
49
|
|
|
49
50
|
_cache: dict[str, tuple[float, list[str]]] = {}
|
|
50
|
-
|
|
51
|
+
#: Shared by every download, each possibly on its own event loop.
|
|
52
|
+
_cache_lock = CrossLoopLock()
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
def _is_public(address: str) -> bool:
|