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.
Files changed (340) hide show
  1. {spotiflac-4.2.1/SpotiFLAC.egg-info → spotiflac-4.2.2}/PKG-INFO +2 -2
  2. spotiflac-4.2.2/SpotiFLAC/api_mixins/signed_sessions.py +55 -0
  3. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/app.py +2 -0
  4. spotiflac-4.2.2/SpotiFLAC/core/cross_loop_lock.py +53 -0
  5. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/dns_doh.py +4 -2
  6. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_cache.py +5 -6
  7. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/lyrics.py +5 -7
  8. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/profiles.py +4 -2
  9. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/progress.py +4 -24
  10. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/provider_stats.py +5 -2
  11. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/session_memory.py +3 -1
  12. spotiflac-4.2.2/SpotiFLAC/core/signed_session_status.py +438 -0
  13. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/app.js +149 -0
  14. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/index.html +21 -4
  15. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/web-shim.js +1 -0
  16. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/launcher.py +58 -0
  17. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/app.py +4 -0
  18. spotiflac-4.2.2/SpotiFLAC/tui/signed_sessions_view.py +135 -0
  19. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/spotiflac.tcss +2 -0
  20. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapp.py +10 -3
  21. {spotiflac-4.2.1 → spotiflac-4.2.2/SpotiFLAC.egg-info}/PKG-INFO +2 -2
  22. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/SOURCES.txt +7 -0
  23. {spotiflac-4.2.1 → spotiflac-4.2.2}/pyproject.toml +2 -2
  24. spotiflac-4.2.2/tests/test_cross_loop_locks.py +110 -0
  25. spotiflac-4.2.2/tests/test_signed_session_status.py +305 -0
  26. spotiflac-4.2.2/tests/test_tui_signed_sessions.py +116 -0
  27. {spotiflac-4.2.1 → spotiflac-4.2.2}/LICENSE +0 -0
  28. {spotiflac-4.2.1 → spotiflac-4.2.2}/MANIFEST.in +0 -0
  29. {spotiflac-4.2.1 → spotiflac-4.2.2}/README.md +0 -0
  30. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/__init__.py +0 -0
  31. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/__main__.py +0 -0
  32. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/__init__.py +0 -0
  33. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/covers_lyrics.py +0 -0
  34. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/csv_import.py +0 -0
  35. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/dedup.py +0 -0
  36. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/discovery.py +0 -0
  37. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/extension_health.py +0 -0
  38. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/failed_tracks.py +0 -0
  39. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/local_tagging.py +0 -0
  40. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/search.py +0 -0
  41. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/stats.py +0 -0
  42. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/subscriptions.py +0 -0
  43. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/api_mixins/trust.py +0 -0
  44. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/check_update.py +0 -0
  45. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/client.py +0 -0
  46. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/__init__.py +0 -0
  47. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/acoustid_lookup.py +0 -0
  48. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/apple_music_metadata.py +0 -0
  49. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/apple_ttml.py +0 -0
  50. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/atomic_io.py +0 -0
  51. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/audio_fingerprint.py +0 -0
  52. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/base.py +0 -0
  53. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/cache_admin.py +0 -0
  54. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/canvas.py +0 -0
  55. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/cli_preview.py +0 -0
  56. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/code_search.py +0 -0
  57. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/console.py +0 -0
  58. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/csv_picker.py +0 -0
  59. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/csv_source.py +0 -0
  60. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/db.py +0 -0
  61. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/download_log.py +0 -0
  62. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/download_validation.py +0 -0
  63. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/errors.py +0 -0
  64. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/extension_enrichment.py +0 -0
  65. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/extension_metadata.py +0 -0
  66. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/failed_tracks.py +0 -0
  67. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/ffmpeg_check.py +0 -0
  68. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/flac_validation.py +0 -0
  69. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/health_check.py +0 -0
  70. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/hires_check.py +0 -0
  71. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/history.py +0 -0
  72. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/hooks.py +0 -0
  73. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/http.py +0 -0
  74. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_finder.py +0 -0
  75. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_helper.py +0 -0
  76. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/isrc_utils.py +0 -0
  77. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/job_queue.py +0 -0
  78. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_dedup.py +0 -0
  79. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_index_cache.py +0 -0
  80. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_notify.py +0 -0
  81. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/library_upgrade.py +0 -0
  82. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/link_resolver.py +0 -0
  83. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/local_matcher.py +0 -0
  84. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/local_processor.py +0 -0
  85. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/local_scanner.py +0 -0
  86. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/loop_runner.py +0 -0
  87. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/metadata_enrichment.py +0 -0
  88. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/metadata_fallback.py +0 -0
  89. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/models.py +0 -0
  90. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/musicbrainz.py +0 -0
  91. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/node_check.py +0 -0
  92. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/notifiers.py +0 -0
  93. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/output_lock.py +0 -0
  94. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/output_sink.py +0 -0
  95. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/paths.py +0 -0
  96. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/playlist_sync.py +0 -0
  97. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/quality.py +0 -0
  98. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/recording_guard.py +0 -0
  99. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/replaygain.py +0 -0
  100. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/report.py +0 -0
  101. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/response_cache.py +0 -0
  102. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_desktop.py +0 -0
  103. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_errors.py +0 -0
  104. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_mobile.py +0 -0
  105. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/signed_session_mono.py +0 -0
  106. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/solver.py +0 -0
  107. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/soundplate.py +0 -0
  108. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotfetch.py +0 -0
  109. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_metadata.py +0 -0
  110. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_protobuf.py +0 -0
  111. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/spotify_totp.py +0 -0
  112. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/stats.py +0 -0
  113. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/subscription_scheduler.py +0 -0
  114. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/subscriptions.py +0 -0
  115. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/tagger.py +0 -0
  116. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/text_match.py +0 -0
  117. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/tidal_metadata.py +0 -0
  118. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/tracklist.py +0 -0
  119. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/transcode.py +0 -0
  120. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/url_utils.py +0 -0
  121. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/core/web_users.py +0 -0
  122. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/downloader.py +0 -0
  123. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/__init__.py +0 -0
  124. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/_bridge.js +0 -0
  125. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/_fsguard.js +0 -0
  126. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/_netguard.js +0 -0
  127. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/catalog.py +0 -0
  128. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/directories.py +0 -0
  129. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/manager.py +0 -0
  130. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/provider.py +0 -0
  131. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/python_provider.py +0 -0
  132. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/registry_config.py +0 -0
  133. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/runtime.py +0 -0
  134. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/runtime_features.py +0 -0
  135. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/sandbox.py +0 -0
  136. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/extensions/trust.py +0 -0
  137. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/am.png +0 -0
  138. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/amzn.png +0 -0
  139. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/apple-touch-icon.png +0 -0
  140. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/download.svg +0 -0
  141. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/dzr.png +0 -0
  142. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/flacdownloader.png +0 -0
  143. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/joox.svg +0 -0
  144. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/kuwo.png +0 -0
  145. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo-192.png +0 -0
  146. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo-512.png +0 -0
  147. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/logo.svg +0 -0
  148. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/lrclib.png +0 -0
  149. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/migu.jpeg +0 -0
  150. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musicbrainz_d.png +0 -0
  151. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musicbrainz_l.png +0 -0
  152. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/musixmatch.svg +0 -0
  153. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/netease.svg +0 -0
  154. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/next.svg +0 -0
  155. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/pandora.svg +0 -0
  156. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/qbz.png +0 -0
  157. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songlink_d.png +0 -0
  158. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songlink_l.png +0 -0
  159. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/songstats.png +0 -0
  160. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/soundcloud.svg +0 -0
  161. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/spotify.svg +0 -0
  162. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/spotubedl.svg +0 -0
  163. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/tidal_d.png +0 -0
  164. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/tidal_l.png +0 -0
  165. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/xbatchdl.svg +0 -0
  166. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/assets/icons/youtube.svg +0 -0
  167. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/manifest.json +0 -0
  168. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/styles.css +0 -0
  169. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/sw.js +0 -0
  170. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/frontend/toast-system.js +0 -0
  171. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/py.typed +0 -0
  172. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/__init__.py +0 -0
  173. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/bump_cache_bust.py +0 -0
  174. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/cloud_config_cli.py +0 -0
  175. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/dedup_check_cli.py +0 -0
  176. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/ext_dryrun.py +0 -0
  177. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/ext_scaffold.py +0 -0
  178. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/hires_check_cli.py +0 -0
  179. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/jellyfin_lyrics_gap.py +0 -0
  180. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/library_dedup_cli.py +0 -0
  181. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/library_upgrade_cli.py +0 -0
  182. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tools/registry_signing_cli.py +0 -0
  183. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/__init__.py +0 -0
  184. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/banner.py +0 -0
  185. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/branding.py +0 -0
  186. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/clipboard.py +0 -0
  187. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/config_state.py +0 -0
  188. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/config_view.py +0 -0
  189. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/csv_picker_screen.py +0 -0
  190. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/extensions_view.py +0 -0
  191. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/health_view.py +0 -0
  192. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/help_screen.py +0 -0
  193. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/queue_view.py +0 -0
  194. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/runner.py +0 -0
  195. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/search_view.py +0 -0
  196. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/session_view.py +0 -0
  197. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/tracklist_view.py +0 -0
  198. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/tui/window_size.py +0 -0
  199. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapi/__init__.py +0 -0
  200. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapi/routes.py +0 -0
  201. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC/webapi/schemas.py +0 -0
  202. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/dependency_links.txt +0 -0
  203. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/entry_points.txt +0 -0
  204. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/requires.txt +0 -0
  205. {spotiflac-4.2.1 → spotiflac-4.2.2}/SpotiFLAC.egg-info/top_level.txt +0 -0
  206. {spotiflac-4.2.1 → spotiflac-4.2.2}/setup.cfg +0 -0
  207. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_acoustid_lookup.py +0 -0
  208. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_app_api_mixins.py +0 -0
  209. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_artist_credit_split.py +0 -0
  210. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_audio_fingerprint.py +0 -0
  211. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_download_segments.py +0 -0
  212. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_mobile_api.py +0 -0
  213. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_retry_utils.py +0 -0
  214. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_bridge_startup_errors.py +0 -0
  215. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cache_admin.py +0 -0
  216. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_canvas.py +0 -0
  217. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_canvas_sidecar.py +0 -0
  218. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cli_preview.py +0 -0
  219. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_core_logic.py +0 -0
  220. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cover_fitting.py +0 -0
  221. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_cover_url_plumbing.py +0 -0
  222. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_credential_storage.py +0 -0
  223. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_csv_download.py +0 -0
  224. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_csv_picker.py +0 -0
  225. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_csv_source.py +0 -0
  226. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_db_persistence.py +0 -0
  227. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_dedup_mixin.py +0 -0
  228. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_directories.py +0 -0
  229. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_disc_number_from_album.py +0 -0
  230. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_discovery_mixin.py +0 -0
  231. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_dns_doh.py +0 -0
  232. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_queue_reporting.py +0 -0
  233. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_recording_guard_wiring.py +0 -0
  234. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_resume.py +0 -0
  235. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_download_services.py +0 -0
  236. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_ext_scaffold_and_dryrun.py +0 -0
  237. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_enrichment.py +0 -0
  238. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_fsguard.py +0 -0
  239. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_install_limits.py +0 -0
  240. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_metadata.py +0 -0
  241. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_netguard.py +0 -0
  242. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_output_containment.py +0 -0
  243. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_extension_sandbox.py +0 -0
  244. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_failed_tracks_and_resume.py +0 -0
  245. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_fake_hires_redownload.py +0 -0
  246. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_ffmpeg_check.py +0 -0
  247. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_frontend_new_ui_wiring.py +0 -0
  248. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_batch_downloads.py +0 -0
  249. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_folder_options.py +0 -0
  250. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_hires_verification.py +0 -0
  251. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_gui_prefetched_selection.py +0 -0
  252. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_hires_check.py +0 -0
  253. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_http_redirects.py +0 -0
  254. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_job_queue.py +0 -0
  255. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_json_report.py +0 -0
  256. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_launcher_tui_flag.py +0 -0
  257. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_launcher_web_flags.py +0 -0
  258. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_dedup.py +0 -0
  259. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_index_cache.py +0 -0
  260. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_notify.py +0 -0
  261. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_library_upgrade.py +0 -0
  262. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_link_resolver_resolve_api.py +0 -0
  263. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_link_resolver_songstats.py +0 -0
  264. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_local_matcher.py +0 -0
  265. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_loop_runner.py +0 -0
  266. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lrc_sidecar.py +0 -0
  267. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lyrics_apple_match.py +0 -0
  268. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lyrics_jellyfin_gap.py +0 -0
  269. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_lyrics_provider_order.py +0 -0
  270. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_m4a_no_roundtrip.py +0 -0
  271. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_metadata_fallback.py +0 -0
  272. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_mono_browser_cleanup.py +0 -0
  273. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_mono_page_fallback.py +0 -0
  274. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_musicbrainz_fallback.py +0 -0
  275. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_musicbrainz_release_pick.py +0 -0
  276. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_musicbrainz_tag_names.py +0 -0
  277. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_node_check.py +0 -0
  278. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_node_options_quoting.py +0 -0
  279. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_notifiers.py +0 -0
  280. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_output_lock.py +0 -0
  281. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_output_sink.py +0 -0
  282. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_playcount_throttle.py +0 -0
  283. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_post_download_command_gate.py +0 -0
  284. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_post_download_hooks.py +0 -0
  285. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_profiles.py +0 -0
  286. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_progress_and_csv_counters.py +0 -0
  287. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_progress_cross_loop.py +0 -0
  288. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_progress_manager_loops.py +0 -0
  289. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_quotas.py +0 -0
  290. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_recording_guard.py +0 -0
  291. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_registry_signing_cli.py +0 -0
  292. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_release_date_hydration.py +0 -0
  293. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_replaygain.py +0 -0
  294. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_runtime_node_check.py +0 -0
  295. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_search_mixin.py +0 -0
  296. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_signed_session_backoff.py +0 -0
  297. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_signed_session_errors.py +0 -0
  298. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_smoke.py +0 -0
  299. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_solver_turnstile_locate.py +0 -0
  300. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_solver_virtual_display.py +0 -0
  301. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_spotify_metadata_nulls.py +0 -0
  302. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_stats.py +0 -0
  303. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_stream_url_validation.py +0 -0
  304. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_subscription_scheduler.py +0 -0
  305. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_subscriptions.py +0 -0
  306. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_subscriptions_mixin.py +0 -0
  307. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tag_number_parsing.py +0 -0
  308. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_title_floor.py +0 -0
  309. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tqdm_lock_under_capture.py +0 -0
  310. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_track_identity.py +0 -0
  311. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_track_isrc_wiring.py +0 -0
  312. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tracklist.py +0 -0
  313. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_transcode_ffmpeg_install.py +0 -0
  314. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_transcode_lossless.py +0 -0
  315. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_trust.py +0 -0
  316. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_trust_enforcement.py +0 -0
  317. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_trust_mixin.py +0 -0
  318. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_app.py +0 -0
  319. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_config_profile.py +0 -0
  320. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_config_state.py +0 -0
  321. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_csv_picker.py +0 -0
  322. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_extensions.py +0 -0
  323. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_fixes.py +0 -0
  324. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_health.py +0 -0
  325. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_look.py +0 -0
  326. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_runner.py +0 -0
  327. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_search.py +0 -0
  328. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_session.py +0 -0
  329. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_tracklist.py +0 -0
  330. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_tui_window_size.py +0 -0
  331. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_watch.py +0 -0
  332. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_web_shim_methods_in_sync.py +0 -0
  333. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_web_users.py +0 -0
  334. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapi_integration.py +0 -0
  335. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapi_v1.py +0 -0
  336. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_auth.py +0 -0
  337. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_isolation.py +0 -0
  338. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_multiuser.py +0 -0
  339. {spotiflac-4.2.1 → spotiflac-4.2.2}/tests/test_webapp_ops.py +0 -0
  340. {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.1
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, ShuKurenais
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
- _cache_lock = asyncio.Lock()
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
- _cache_lock: asyncio.Lock | None = None
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() -> asyncio.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
- _spotify_token_lock: asyncio.Lock | None = None
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() -> asyncio.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
- # Using asyncio.Lock instead of threading.Lock so it doesn't block the event loop
24
- _io_lock = asyncio.Lock()
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
- A threading.Lock has no loop affinity. It is acquired without blocking
385
- and the caller yields while it is busy, so the loop is never held up —
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
- self._stats_lock = asyncio.Lock()
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 = asyncio.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
- _io_lock = asyncio.Lock()
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