SpotiFLAC 4.2.1__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.1/SpotiFLAC.egg-info → spotiflac-4.2.2}/PKG-INFO +2 -2
- spotiflac-4.2.2/SpotiFLAC/api_mixins/signed_sessions.py +55 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/app.py +2 -0
- spotiflac-4.2.2/SpotiFLAC/core/cross_loop_lock.py +53 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/dns_doh.py +4 -2
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_cache.py +5 -6
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/lyrics.py +5 -7
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/profiles.py +4 -2
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/progress.py +4 -24
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/provider_stats.py +5 -2
- {spotiflac-4.2.1 → 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.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/app.js +149 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/index.html +21 -4
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/web-shim.js +1 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/launcher.py +58 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/app.py +4 -0
- spotiflac-4.2.2/SpotiFLAC/tui/signed_sessions_view.py +135 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/spotiflac.tcss +2 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapp.py +10 -3
- {spotiflac-4.2.1 → spotiflac-4.2.2/SpotiFLAC.egg-info}/PKG-INFO +2 -2
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/SOURCES.txt +7 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/pyproject.toml +2 -2
- spotiflac-4.2.2/tests/test_cross_loop_locks.py +110 -0
- spotiflac-4.2.2/tests/test_signed_session_status.py +305 -0
- spotiflac-4.2.2/tests/test_tui_signed_sessions.py +116 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/LICENSE +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/MANIFEST.in +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/README.md +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/__init__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/__main__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/__init__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/covers_lyrics.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/csv_import.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/dedup.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/discovery.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/extension_health.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/failed_tracks.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/local_tagging.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/search.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/stats.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/subscriptions.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/trust.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/check_update.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/client.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/__init__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/acoustid_lookup.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/apple_music_metadata.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/apple_ttml.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/atomic_io.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/audio_fingerprint.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/base.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/cache_admin.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/canvas.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/cli_preview.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/code_search.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/console.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/csv_picker.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/csv_source.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/db.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/download_log.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/download_validation.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/errors.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/extension_enrichment.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/extension_metadata.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/failed_tracks.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/ffmpeg_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/flac_validation.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/health_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/hires_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/history.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/hooks.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/http.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_finder.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_helper.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_utils.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/job_queue.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_dedup.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_index_cache.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_notify.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_upgrade.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/link_resolver.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/local_matcher.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/local_processor.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/local_scanner.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/loop_runner.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/metadata_enrichment.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/metadata_fallback.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/models.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/musicbrainz.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/node_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/notifiers.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/output_lock.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/output_sink.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/paths.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/playlist_sync.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/quality.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/recording_guard.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/replaygain.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/report.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/response_cache.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_desktop.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_errors.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_mobile.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_mono.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/solver.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/soundplate.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotfetch.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_metadata.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_protobuf.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_totp.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/stats.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/subscription_scheduler.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/subscriptions.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/tagger.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/text_match.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/tidal_metadata.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/tracklist.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/transcode.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/url_utils.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/web_users.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/downloader.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/__init__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/_bridge.js +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/_fsguard.js +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/_netguard.js +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/catalog.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/directories.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/manager.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/provider.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/python_provider.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/registry_config.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/runtime.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/runtime_features.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/sandbox.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/trust.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/am.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/amzn.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/apple-touch-icon.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/download.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/dzr.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/flacdownloader.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/joox.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/kuwo.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo-192.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo-512.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/lrclib.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/migu.jpeg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musicbrainz_d.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musicbrainz_l.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musixmatch.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/netease.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/next.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/pandora.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/qbz.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songlink_d.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songlink_l.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songstats.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/soundcloud.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/spotify.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/spotubedl.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/tidal_d.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/tidal_l.png +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/xbatchdl.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/youtube.svg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/manifest.json +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/styles.css +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/sw.js +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/toast-system.js +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/py.typed +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/__init__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/bump_cache_bust.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/cloud_config_cli.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/dedup_check_cli.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/ext_dryrun.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/ext_scaffold.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/hires_check_cli.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/jellyfin_lyrics_gap.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/library_dedup_cli.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/library_upgrade_cli.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/registry_signing_cli.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/__init__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/banner.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/branding.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/clipboard.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/config_state.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/config_view.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/csv_picker_screen.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/extensions_view.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/health_view.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/help_screen.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/queue_view.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/runner.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/search_view.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/session_view.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/tracklist_view.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/window_size.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapi/__init__.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapi/routes.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapi/schemas.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/dependency_links.txt +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/entry_points.txt +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/requires.txt +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/top_level.txt +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/setup.cfg +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_acoustid_lookup.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_app_api_mixins.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_artist_credit_split.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_audio_fingerprint.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_download_segments.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_mobile_api.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_retry_utils.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_startup_errors.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cache_admin.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_canvas.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_canvas_sidecar.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cli_preview.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_core_logic.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cover_fitting.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cover_url_plumbing.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_credential_storage.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_csv_download.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_csv_picker.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_csv_source.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_db_persistence.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_dedup_mixin.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_directories.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_disc_number_from_album.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_discovery_mixin.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_dns_doh.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_queue_reporting.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_recording_guard_wiring.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_resume.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_services.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_ext_scaffold_and_dryrun.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_enrichment.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_fsguard.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_install_limits.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_metadata.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_netguard.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_output_containment.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_sandbox.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_failed_tracks_and_resume.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_fake_hires_redownload.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_ffmpeg_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_frontend_new_ui_wiring.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_batch_downloads.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_folder_options.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_hires_verification.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_prefetched_selection.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_hires_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_http_redirects.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_job_queue.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_json_report.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_launcher_tui_flag.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_launcher_web_flags.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_dedup.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_index_cache.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_notify.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_upgrade.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_link_resolver_resolve_api.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_link_resolver_songstats.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_local_matcher.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_loop_runner.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lrc_sidecar.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lyrics_apple_match.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lyrics_jellyfin_gap.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lyrics_provider_order.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_m4a_no_roundtrip.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_metadata_fallback.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_mono_browser_cleanup.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_mono_page_fallback.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_musicbrainz_fallback.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_musicbrainz_release_pick.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_musicbrainz_tag_names.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_node_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_node_options_quoting.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_notifiers.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_output_lock.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_output_sink.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_playcount_throttle.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_post_download_command_gate.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_post_download_hooks.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_profiles.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_progress_and_csv_counters.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_progress_cross_loop.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_progress_manager_loops.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_quotas.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_recording_guard.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_registry_signing_cli.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_release_date_hydration.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_replaygain.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_runtime_node_check.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_search_mixin.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_signed_session_backoff.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_signed_session_errors.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_smoke.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_solver_turnstile_locate.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_solver_virtual_display.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_spotify_metadata_nulls.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_stats.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_stream_url_validation.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_subscription_scheduler.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_subscriptions.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_subscriptions_mixin.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tag_number_parsing.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_title_floor.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tqdm_lock_under_capture.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_track_identity.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_track_isrc_wiring.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tracklist.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_transcode_ffmpeg_install.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_transcode_lossless.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_trust.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_trust_enforcement.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_trust_mixin.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_app.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_config_profile.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_config_state.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_csv_picker.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_extensions.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_fixes.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_health.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_look.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_runner.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_search.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_session.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_tracklist.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_window_size.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_watch.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_web_shim_methods_in_sync.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_web_users.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapi_integration.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapi_v1.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_auth.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_isolation.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_multiuser.py +0 -0
- {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_ops.py +0 -0
- {spotiflac-4.2.1 → 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
|
|
@@ -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>`
|
|
@@ -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:
|
|
@@ -11,6 +11,7 @@ import json
|
|
|
11
11
|
import logging
|
|
12
12
|
import time
|
|
13
13
|
|
|
14
|
+
from .cross_loop_lock import CrossLoopLock
|
|
14
15
|
from .paths import cache_path
|
|
15
16
|
|
|
16
17
|
try:
|
|
@@ -21,15 +22,13 @@ except ImportError:
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
_CACHE_FILE = cache_path("isrc-cache.json")
|
|
24
|
-
|
|
25
|
+
#: Not an asyncio.Lock: this cache is shared by every download, and under
|
|
26
|
+
#: --web each one runs its own event loop. See core/cross_loop_lock.py.
|
|
27
|
+
_cache_lock = CrossLoopLock()
|
|
25
28
|
_cache: dict[str, dict] | None = None
|
|
26
29
|
|
|
27
30
|
|
|
28
|
-
async def _get_lock() ->
|
|
29
|
-
"""Get or create the asyncio.Lock for cache access."""
|
|
30
|
-
global _cache_lock
|
|
31
|
-
if _cache_lock is None:
|
|
32
|
-
_cache_lock = asyncio.Lock()
|
|
31
|
+
async def _get_lock() -> CrossLoopLock:
|
|
33
32
|
return _cache_lock
|
|
34
33
|
|
|
35
34
|
|
|
@@ -12,6 +12,7 @@ from dataclasses import dataclass
|
|
|
12
12
|
from typing import TYPE_CHECKING
|
|
13
13
|
|
|
14
14
|
from . import get_amazon_endpoint
|
|
15
|
+
from .cross_loop_lock import CrossLoopLock
|
|
15
16
|
from .http import NetworkManager
|
|
16
17
|
from .response_cache import get as get_cached_response
|
|
17
18
|
from .response_cache import put as put_cached_response
|
|
@@ -183,15 +184,12 @@ _LYRICS_MISS_CACHE_TTL = 6 * 60 * 60
|
|
|
183
184
|
# ---------------------------------------------------------------------------
|
|
184
185
|
|
|
185
186
|
_spotify_session_cache: dict[str, object] = {}
|
|
186
|
-
|
|
187
|
+
#: Every track's lyrics lookup goes through here, from whichever event loop
|
|
188
|
+
#: that download runs on — an asyncio.Lock would bind to the first one.
|
|
189
|
+
_spotify_token_lock = CrossLoopLock()
|
|
187
190
|
|
|
188
191
|
|
|
189
|
-
async def _get_spotify_lock() ->
|
|
190
|
-
global _spotify_token_lock
|
|
191
|
-
|
|
192
|
-
if _spotify_token_lock is None:
|
|
193
|
-
_spotify_token_lock = asyncio.Lock()
|
|
194
|
-
|
|
192
|
+
async def _get_spotify_lock() -> CrossLoopLock:
|
|
195
193
|
return _spotify_token_lock
|
|
196
194
|
|
|
197
195
|
|
|
@@ -16,12 +16,14 @@ import time
|
|
|
16
16
|
|
|
17
17
|
from pydantic import BaseModel, Field, ValidationError, field_validator
|
|
18
18
|
|
|
19
|
+
from .cross_loop_lock import CrossLoopLock
|
|
19
20
|
from .paths import adopt_legacy_cache_file, cache_path
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
|
-
#
|
|
24
|
-
|
|
24
|
+
# Doesn't block the event loop, and — unlike an asyncio.Lock — works from
|
|
25
|
+
# every loop this process runs. See core/cross_loop_lock.py.
|
|
26
|
+
_io_lock = CrossLoopLock()
|
|
25
27
|
_PROFILES_FILE = cache_path("profiles.json")
|
|
26
28
|
|
|
27
29
|
|
|
@@ -8,7 +8,6 @@ import io
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
|
-
import threading
|
|
12
11
|
import time
|
|
13
12
|
from dataclasses import dataclass
|
|
14
13
|
from enum import Enum
|
|
@@ -18,6 +17,7 @@ from tqdm import tqdm
|
|
|
18
17
|
from typing_extensions import Self
|
|
19
18
|
|
|
20
19
|
from .console import print_track_progress
|
|
20
|
+
from .cross_loop_lock import CrossLoopLock
|
|
21
21
|
from .output_sink import STDERR, STDOUT, emit, sink_active, use_thread_only_tqdm_lock
|
|
22
22
|
|
|
23
23
|
# tqdm.get_lock() remains the native tqdm lock (this is not our own hack;
|
|
@@ -362,7 +362,7 @@ class DownloadItem:
|
|
|
362
362
|
cover_url: str = ""
|
|
363
363
|
|
|
364
364
|
|
|
365
|
-
class _CrossLoopLock:
|
|
365
|
+
class _CrossLoopLock(CrossLoopLock):
|
|
366
366
|
"""A mutex that survives more than one event loop.
|
|
367
367
|
|
|
368
368
|
asyncio.Lock binds itself to the loop that first awaits it and raises
|
|
@@ -381,30 +381,10 @@ class _CrossLoopLock:
|
|
|
381
381
|
RuntimeError: <asyncio.locks.Lock ...> is bound to a different
|
|
382
382
|
event loop
|
|
383
383
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
and every critical section it guards is a short synchronous mutation
|
|
387
|
-
with no await in it, so contention is brief by construction.
|
|
384
|
+
The implementation now lives in core/cross_loop_lock.py, shared with
|
|
385
|
+
every other process-wide lock that had the same bug.
|
|
388
386
|
"""
|
|
389
387
|
|
|
390
|
-
def __init__(self, poll_interval: float = 0.01) -> None:
|
|
391
|
-
self._lock = threading.Lock()
|
|
392
|
-
self._poll_interval = poll_interval
|
|
393
|
-
|
|
394
|
-
async def __aenter__(self) -> _CrossLoopLock:
|
|
395
|
-
# A plain yield first: an uncontended lock is the overwhelmingly
|
|
396
|
-
# common case, and a contended one is usually free again within a
|
|
397
|
-
# single tick, so the poll interval should be the exception.
|
|
398
|
-
if not self._lock.acquire(blocking=False):
|
|
399
|
-
await asyncio.sleep(0)
|
|
400
|
-
while not self._lock.acquire(blocking=False):
|
|
401
|
-
await asyncio.sleep(self._poll_interval)
|
|
402
|
-
return self
|
|
403
|
-
|
|
404
|
-
async def __aexit__(self, exc_type, exc, tb) -> bool:
|
|
405
|
-
self._lock.release()
|
|
406
|
-
return False
|
|
407
|
-
|
|
408
388
|
|
|
409
389
|
class DownloadBroadcaster:
|
|
410
390
|
"""Singleton for streaming progress events to external listeners
|
|
@@ -7,6 +7,7 @@ from dataclasses import asdict, dataclass
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from .atomic_io import write_json_atomic
|
|
10
|
+
from .cross_loop_lock import CrossLoopLock
|
|
10
11
|
from .paths import cache_path
|
|
11
12
|
|
|
12
13
|
#: Key prefix used by the download path (see downloader.download_one_async),
|
|
@@ -145,9 +146,11 @@ class ProviderScorer:
|
|
|
145
146
|
|
|
146
147
|
def __init__(self) -> None:
|
|
147
148
|
self._stats: dict[str, _ProviderStats] = {}
|
|
148
|
-
|
|
149
|
+
# Cross-loop, not asyncio.Lock: `_scorer` is a process-wide singleton
|
|
150
|
+
# every download records into, each from its own event loop.
|
|
151
|
+
self._stats_lock = CrossLoopLock()
|
|
149
152
|
self._initialized = False
|
|
150
|
-
self._init_lock =
|
|
153
|
+
self._init_lock = CrossLoopLock()
|
|
151
154
|
|
|
152
155
|
async def _ensure_initialized(self) -> None:
|
|
153
156
|
"""Loads the database only the first time it's requested."""
|
|
@@ -5,11 +5,13 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
7
|
|
|
8
|
+
from .cross_loop_lock import CrossLoopLock
|
|
8
9
|
from .paths import adopt_legacy_cache_file, cache_path
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
#: Called from the GUI's shared loop and from each batch's own asyncio.run().
|
|
14
|
+
_io_lock = CrossLoopLock()
|
|
13
15
|
_SESSION_FILE = cache_path("session.json")
|
|
14
16
|
_MAX_HISTORY = 20
|
|
15
17
|
|