mycelium-ai 0.5.0__py3-none-any.whl

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 (164) hide show
  1. mycelium/__init__.py +0 -0
  2. mycelium/api/__init__.py +0 -0
  3. mycelium/api/app.py +1147 -0
  4. mycelium/api/client_app.py +170 -0
  5. mycelium/api/generated_sources/__init__.py +0 -0
  6. mycelium/api/generated_sources/server_schemas/__init__.py +97 -0
  7. mycelium/api/generated_sources/server_schemas/api/__init__.py +5 -0
  8. mycelium/api/generated_sources/server_schemas/api/default_api.py +2473 -0
  9. mycelium/api/generated_sources/server_schemas/api_client.py +766 -0
  10. mycelium/api/generated_sources/server_schemas/api_response.py +25 -0
  11. mycelium/api/generated_sources/server_schemas/configuration.py +434 -0
  12. mycelium/api/generated_sources/server_schemas/exceptions.py +166 -0
  13. mycelium/api/generated_sources/server_schemas/models/__init__.py +41 -0
  14. mycelium/api/generated_sources/server_schemas/models/api_section.py +71 -0
  15. mycelium/api/generated_sources/server_schemas/models/chroma_section.py +69 -0
  16. mycelium/api/generated_sources/server_schemas/models/clap_section.py +75 -0
  17. mycelium/api/generated_sources/server_schemas/models/compute_on_server200_response.py +79 -0
  18. mycelium/api/generated_sources/server_schemas/models/compute_on_server_request.py +67 -0
  19. mycelium/api/generated_sources/server_schemas/models/compute_text_search_request.py +69 -0
  20. mycelium/api/generated_sources/server_schemas/models/config_request.py +81 -0
  21. mycelium/api/generated_sources/server_schemas/models/config_response.py +107 -0
  22. mycelium/api/generated_sources/server_schemas/models/create_playlist_request.py +71 -0
  23. mycelium/api/generated_sources/server_schemas/models/get_similar_by_track200_response.py +143 -0
  24. mycelium/api/generated_sources/server_schemas/models/library_stats_response.py +77 -0
  25. mycelium/api/generated_sources/server_schemas/models/logging_section.py +67 -0
  26. mycelium/api/generated_sources/server_schemas/models/media_server_section.py +67 -0
  27. mycelium/api/generated_sources/server_schemas/models/playlist_response.py +73 -0
  28. mycelium/api/generated_sources/server_schemas/models/plex_section.py +71 -0
  29. mycelium/api/generated_sources/server_schemas/models/processing_response.py +90 -0
  30. mycelium/api/generated_sources/server_schemas/models/save_config_response.py +73 -0
  31. mycelium/api/generated_sources/server_schemas/models/scan_library_response.py +75 -0
  32. mycelium/api/generated_sources/server_schemas/models/search_result_response.py +75 -0
  33. mycelium/api/generated_sources/server_schemas/models/server_section.py +67 -0
  34. mycelium/api/generated_sources/server_schemas/models/stop_processing_response.py +71 -0
  35. mycelium/api/generated_sources/server_schemas/models/task_status_response.py +87 -0
  36. mycelium/api/generated_sources/server_schemas/models/track_database_stats.py +75 -0
  37. mycelium/api/generated_sources/server_schemas/models/track_response.py +77 -0
  38. mycelium/api/generated_sources/server_schemas/models/tracks_list_response.py +81 -0
  39. mycelium/api/generated_sources/server_schemas/rest.py +329 -0
  40. mycelium/api/generated_sources/server_schemas/test/__init__.py +0 -0
  41. mycelium/api/generated_sources/server_schemas/test/test_api_section.py +57 -0
  42. mycelium/api/generated_sources/server_schemas/test/test_chroma_section.py +55 -0
  43. mycelium/api/generated_sources/server_schemas/test/test_clap_section.py +60 -0
  44. mycelium/api/generated_sources/server_schemas/test/test_compute_on_server200_response.py +52 -0
  45. mycelium/api/generated_sources/server_schemas/test/test_compute_on_server_request.py +53 -0
  46. mycelium/api/generated_sources/server_schemas/test/test_compute_text_search_request.py +54 -0
  47. mycelium/api/generated_sources/server_schemas/test/test_config_request.py +66 -0
  48. mycelium/api/generated_sources/server_schemas/test/test_config_response.py +97 -0
  49. mycelium/api/generated_sources/server_schemas/test/test_create_playlist_request.py +60 -0
  50. mycelium/api/generated_sources/server_schemas/test/test_default_api.py +150 -0
  51. mycelium/api/generated_sources/server_schemas/test/test_get_similar_by_track200_response.py +61 -0
  52. mycelium/api/generated_sources/server_schemas/test/test_library_stats_response.py +63 -0
  53. mycelium/api/generated_sources/server_schemas/test/test_logging_section.py +53 -0
  54. mycelium/api/generated_sources/server_schemas/test/test_media_server_section.py +53 -0
  55. mycelium/api/generated_sources/server_schemas/test/test_playlist_response.py +58 -0
  56. mycelium/api/generated_sources/server_schemas/test/test_plex_section.py +56 -0
  57. mycelium/api/generated_sources/server_schemas/test/test_processing_response.py +61 -0
  58. mycelium/api/generated_sources/server_schemas/test/test_save_config_response.py +58 -0
  59. mycelium/api/generated_sources/server_schemas/test/test_scan_library_response.py +61 -0
  60. mycelium/api/generated_sources/server_schemas/test/test_search_result_response.py +69 -0
  61. mycelium/api/generated_sources/server_schemas/test/test_server_section.py +53 -0
  62. mycelium/api/generated_sources/server_schemas/test/test_stop_processing_response.py +55 -0
  63. mycelium/api/generated_sources/server_schemas/test/test_task_status_response.py +71 -0
  64. mycelium/api/generated_sources/server_schemas/test/test_track_database_stats.py +60 -0
  65. mycelium/api/generated_sources/server_schemas/test/test_track_response.py +63 -0
  66. mycelium/api/generated_sources/server_schemas/test/test_tracks_list_response.py +75 -0
  67. mycelium/api/generated_sources/worker_schemas/__init__.py +61 -0
  68. mycelium/api/generated_sources/worker_schemas/api/__init__.py +5 -0
  69. mycelium/api/generated_sources/worker_schemas/api/default_api.py +318 -0
  70. mycelium/api/generated_sources/worker_schemas/api_client.py +766 -0
  71. mycelium/api/generated_sources/worker_schemas/api_response.py +25 -0
  72. mycelium/api/generated_sources/worker_schemas/configuration.py +434 -0
  73. mycelium/api/generated_sources/worker_schemas/exceptions.py +166 -0
  74. mycelium/api/generated_sources/worker_schemas/models/__init__.py +23 -0
  75. mycelium/api/generated_sources/worker_schemas/models/save_config_response.py +73 -0
  76. mycelium/api/generated_sources/worker_schemas/models/worker_clap_section.py +75 -0
  77. mycelium/api/generated_sources/worker_schemas/models/worker_client_api_section.py +69 -0
  78. mycelium/api/generated_sources/worker_schemas/models/worker_client_section.py +79 -0
  79. mycelium/api/generated_sources/worker_schemas/models/worker_config_request.py +73 -0
  80. mycelium/api/generated_sources/worker_schemas/models/worker_config_response.py +89 -0
  81. mycelium/api/generated_sources/worker_schemas/models/worker_logging_section.py +67 -0
  82. mycelium/api/generated_sources/worker_schemas/rest.py +329 -0
  83. mycelium/api/generated_sources/worker_schemas/test/__init__.py +0 -0
  84. mycelium/api/generated_sources/worker_schemas/test/test_default_api.py +45 -0
  85. mycelium/api/generated_sources/worker_schemas/test/test_save_config_response.py +58 -0
  86. mycelium/api/generated_sources/worker_schemas/test/test_worker_clap_section.py +60 -0
  87. mycelium/api/generated_sources/worker_schemas/test/test_worker_client_api_section.py +55 -0
  88. mycelium/api/generated_sources/worker_schemas/test/test_worker_client_section.py +65 -0
  89. mycelium/api/generated_sources/worker_schemas/test/test_worker_config_request.py +59 -0
  90. mycelium/api/generated_sources/worker_schemas/test/test_worker_config_response.py +89 -0
  91. mycelium/api/generated_sources/worker_schemas/test/test_worker_logging_section.py +53 -0
  92. mycelium/api/worker_models.py +99 -0
  93. mycelium/application/__init__.py +11 -0
  94. mycelium/application/job_queue.py +323 -0
  95. mycelium/application/library_management_use_cases.py +292 -0
  96. mycelium/application/search_use_cases.py +96 -0
  97. mycelium/application/services.py +340 -0
  98. mycelium/client.py +554 -0
  99. mycelium/client_config.py +251 -0
  100. mycelium/client_frontend_dist/404.html +1 -0
  101. mycelium/client_frontend_dist/_next/static/a4iyRdfsvkjdyMAK9cE9Y/_buildManifest.js +1 -0
  102. mycelium/client_frontend_dist/_next/static/a4iyRdfsvkjdyMAK9cE9Y/_ssgManifest.js +1 -0
  103. mycelium/client_frontend_dist/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js +1 -0
  104. mycelium/client_frontend_dist/_next/static/chunks/964-830f77d7ce1c2463.js +1 -0
  105. mycelium/client_frontend_dist/_next/static/chunks/app/_not-found/page-d25eede5a9099bd3.js +1 -0
  106. mycelium/client_frontend_dist/_next/static/chunks/app/layout-9b3d32f96dfe13b6.js +1 -0
  107. mycelium/client_frontend_dist/_next/static/chunks/app/page-cc6bad295789134e.js +1 -0
  108. mycelium/client_frontend_dist/_next/static/chunks/framework-7c95b8e5103c9e90.js +1 -0
  109. mycelium/client_frontend_dist/_next/static/chunks/main-6b37be50736577a2.js +1 -0
  110. mycelium/client_frontend_dist/_next/static/chunks/main-app-4153d115599d3126.js +1 -0
  111. mycelium/client_frontend_dist/_next/static/chunks/pages/_app-0a0020ddd67f79cf.js +1 -0
  112. mycelium/client_frontend_dist/_next/static/chunks/pages/_error-03529f2c21436739.js +1 -0
  113. mycelium/client_frontend_dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  114. mycelium/client_frontend_dist/_next/static/chunks/webpack-c81e624915b2ea70.js +1 -0
  115. mycelium/client_frontend_dist/_next/static/css/1eb7f0e2c78e0734.css +1 -0
  116. mycelium/client_frontend_dist/favicon.ico +0 -0
  117. mycelium/client_frontend_dist/file.svg +1 -0
  118. mycelium/client_frontend_dist/globe.svg +1 -0
  119. mycelium/client_frontend_dist/index.html +1 -0
  120. mycelium/client_frontend_dist/index.txt +20 -0
  121. mycelium/client_frontend_dist/next.svg +1 -0
  122. mycelium/client_frontend_dist/vercel.svg +1 -0
  123. mycelium/client_frontend_dist/window.svg +1 -0
  124. mycelium/config.py +346 -0
  125. mycelium/domain/__init__.py +13 -0
  126. mycelium/domain/models.py +71 -0
  127. mycelium/domain/repositories.py +98 -0
  128. mycelium/domain/worker.py +77 -0
  129. mycelium/frontend_dist/404.html +1 -0
  130. mycelium/frontend_dist/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js +1 -0
  131. mycelium/frontend_dist/_next/static/chunks/964-830f77d7ce1c2463.js +1 -0
  132. mycelium/frontend_dist/_next/static/chunks/app/_not-found/page-d25eede5a9099bd3.js +1 -0
  133. mycelium/frontend_dist/_next/static/chunks/app/layout-9b3d32f96dfe13b6.js +1 -0
  134. mycelium/frontend_dist/_next/static/chunks/app/page-a761463485e0540b.js +1 -0
  135. mycelium/frontend_dist/_next/static/chunks/framework-7c95b8e5103c9e90.js +1 -0
  136. mycelium/frontend_dist/_next/static/chunks/main-6b37be50736577a2.js +1 -0
  137. mycelium/frontend_dist/_next/static/chunks/main-app-4153d115599d3126.js +1 -0
  138. mycelium/frontend_dist/_next/static/chunks/pages/_app-0a0020ddd67f79cf.js +1 -0
  139. mycelium/frontend_dist/_next/static/chunks/pages/_error-03529f2c21436739.js +1 -0
  140. mycelium/frontend_dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  141. mycelium/frontend_dist/_next/static/chunks/webpack-c81e624915b2ea70.js +1 -0
  142. mycelium/frontend_dist/_next/static/css/1eb7f0e2c78e0734.css +1 -0
  143. mycelium/frontend_dist/_next/static/glVJ0yJSL0zWN7anTTG3_/_buildManifest.js +1 -0
  144. mycelium/frontend_dist/_next/static/glVJ0yJSL0zWN7anTTG3_/_ssgManifest.js +1 -0
  145. mycelium/frontend_dist/favicon.ico +0 -0
  146. mycelium/frontend_dist/file.svg +1 -0
  147. mycelium/frontend_dist/globe.svg +1 -0
  148. mycelium/frontend_dist/index.html +10 -0
  149. mycelium/frontend_dist/index.txt +20 -0
  150. mycelium/frontend_dist/next.svg +1 -0
  151. mycelium/frontend_dist/vercel.svg +1 -0
  152. mycelium/frontend_dist/window.svg +1 -0
  153. mycelium/infrastructure/__init__.py +17 -0
  154. mycelium/infrastructure/chroma_adapter.py +232 -0
  155. mycelium/infrastructure/clap_adapter.py +280 -0
  156. mycelium/infrastructure/plex_adapter.py +145 -0
  157. mycelium/infrastructure/track_database.py +467 -0
  158. mycelium/main.py +183 -0
  159. mycelium_ai-0.5.0.dist-info/METADATA +312 -0
  160. mycelium_ai-0.5.0.dist-info/RECORD +164 -0
  161. mycelium_ai-0.5.0.dist-info/WHEEL +5 -0
  162. mycelium_ai-0.5.0.dist-info/entry_points.txt +2 -0
  163. mycelium_ai-0.5.0.dist-info/licenses/LICENSE +21 -0
  164. mycelium_ai-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,340 @@
1
+ """Application services for orchestrating business logic."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import List, Optional, Dict, Any
6
+
7
+ from mycelium.application.search_use_cases import (
8
+ MusicSearchUseCase
9
+ )
10
+ from mycelium.application.library_management_use_cases import (
11
+ LibraryScanUseCase,
12
+ EmbeddingProcessingUseCase,
13
+ ProcessingProgressUseCase,
14
+ WorkerBasedProcessingUseCase
15
+ )
16
+ from mycelium.config import MyceliumConfig
17
+ from mycelium.domain.models import Playlist
18
+ from mycelium.domain.models import Track, TrackEmbedding, SearchResult
19
+ from mycelium.infrastructure.plex_adapter import PlexMusicRepository
20
+ from mycelium.infrastructure.clap_adapter import CLAPEmbeddingGenerator
21
+ from mycelium.infrastructure.chroma_adapter import ChromaEmbeddingRepository
22
+ from mycelium.infrastructure import (
23
+ TrackDatabase
24
+ )
25
+
26
+
27
+ class MyceliumService:
28
+ """Main service for orchestrating the Mycelium application."""
29
+
30
+ def __init__(
31
+ self,
32
+ config: MyceliumConfig
33
+ ):
34
+ self._config = config
35
+ self.worker_processing = None
36
+ self.logger = logging.getLogger(__name__)
37
+
38
+ # Initialize repositories and adapters
39
+ self.media_server_repository = PlexMusicRepository(
40
+ plex_url=config.plex.url,
41
+ plex_token=config.plex.token,
42
+ music_library_name=config.plex.music_library_name
43
+ )
44
+
45
+ self.embedding_generator = CLAPEmbeddingGenerator(model_id=config.clap.model_id,
46
+ target_sr=config.clap.target_sr,
47
+ chunk_duration_s=config.clap.chunk_duration_s,
48
+ num_chunks=config.clap.num_chunks,
49
+ max_load_duration_s=config.clap.max_load_duration_s)
50
+
51
+ self.embedding_repository = ChromaEmbeddingRepository(
52
+ db_path=config.chroma.get_db_path(),
53
+ collection_name=config.chroma.collection_name,
54
+ model_id=config.clap.model_id,
55
+ batch_size=config.chroma.batch_size,
56
+ media_server_type=config.media_server.type
57
+ )
58
+
59
+ self.track_database = TrackDatabase(db_path=config.database.get_db_path(),
60
+ media_server_type=config.media_server.type)
61
+
62
+ self.music_search = MusicSearchUseCase(
63
+ embedding_repository=self.embedding_repository,
64
+ embedding_generator=self.embedding_generator
65
+ )
66
+
67
+ self.separated_scan = LibraryScanUseCase(
68
+ media_server_repository=self.media_server_repository,
69
+ track_database=self.track_database
70
+ )
71
+ self.resumable_processing = EmbeddingProcessingUseCase(
72
+ embedding_generator=self.embedding_generator,
73
+ embedding_repository=self.embedding_repository,
74
+ track_database=self.track_database,
75
+ model_id=config.clap.model_id,
76
+ gpu_batch_size=config.server.gpu_batch_size
77
+ )
78
+ self.progress_tracker = ProcessingProgressUseCase(track_database=self.track_database)
79
+
80
+ # Processing state tracking
81
+ self._processing_in_progress = False
82
+
83
+
84
+
85
+ def scan_library_to_database(self, progress_callback: Optional[callable] = None) -> Dict[str, Any]:
86
+ """Scan the Plex library and store metadata to database."""
87
+ return self.separated_scan.execute(progress_callback)
88
+
89
+ def process_embeddings_from_database(
90
+ self,
91
+ progress_callback: Optional[callable] = None,
92
+ max_tracks: Optional[int] = None
93
+ ) -> Dict[str, Any]:
94
+ """Process embeddings for unprocessed tracks from database."""
95
+ if self._processing_in_progress:
96
+ return {
97
+ "message": "Processing is already in progress",
98
+ "status": "already_running"
99
+ }
100
+
101
+ self._processing_in_progress = True
102
+ try:
103
+ # Reset stop flag for new session
104
+ self.reset_processing_stop_flag()
105
+ result = self.resumable_processing.process_embeddings(progress_callback=progress_callback,
106
+ max_tracks=max_tracks)
107
+ return result
108
+ finally:
109
+ self._processing_in_progress = False
110
+
111
+ def stop_processing(self) -> None:
112
+ """Stop the current embedding processing."""
113
+ # Stop server-side processing
114
+ self.resumable_processing.stop()
115
+
116
+ # Stop worker-based processing if available
117
+ if hasattr(self, 'worker_processing'):
118
+ self.stop_worker_processing()
119
+
120
+ def reset_processing_stop_flag(self) -> None:
121
+ """Reset the stop flag for new processing session."""
122
+ self.resumable_processing.reset_stop_flag()
123
+
124
+ def is_processing_active(self) -> bool:
125
+ """Check if any processing is currently active (server or worker)."""
126
+ return self._processing_in_progress or self.has_active_worker_processing()
127
+
128
+ def get_processing_progress(self, model_id: Optional[str] = None) -> Dict[str, Any]:
129
+ """Get current processing progress and statistics."""
130
+ if model_id is None:
131
+ model_id = self.embedding_repository.model_id
132
+ stats = self.progress_tracker.get_current_stats(model_id)
133
+ # Processing is active if either server-side processing is running OR workers have active tasks
134
+ stats["is_processing"] = self.is_processing_active()
135
+ return stats
136
+
137
+ def search_similar_by_audio(
138
+ self,
139
+ filepath: Path,
140
+ n_results: int = 10
141
+ ) -> List[SearchResult]:
142
+ """Search for similar tracks by audio file."""
143
+ return self.music_search.search_by_audio_file(filepath, n_results)
144
+
145
+ def search_similar_by_text(
146
+ self,
147
+ query: str,
148
+ n_results: int = 10
149
+ ) -> List[SearchResult]:
150
+ """Search for tracks by text description."""
151
+ return self.music_search.search_by_text(query, n_results)
152
+
153
+ def search_similar_by_track_id(
154
+ self,
155
+ track_id: str,
156
+ n_results: int = 10
157
+ ) -> List[SearchResult]:
158
+ """Search for tracks similar to a given track ID."""
159
+ return self.music_search.search_by_track_id(track_id=track_id,
160
+ n_results=n_results)
161
+
162
+ def get_database_stats(self) -> dict:
163
+ """Get statistics about the current databases."""
164
+ processing_stats = self.get_processing_progress()
165
+ return {
166
+ "total_embeddings": self.embedding_repository.get_embedding_count(),
167
+ "collection_name": self.embedding_repository.collection_name,
168
+ "database_path": self.embedding_repository.db_path,
169
+ "track_database_stats": processing_stats
170
+ }
171
+
172
+ def get_track_by_id(self, track_id: str) -> Optional[Track]:
173
+ """Get track information by Plex rating key."""
174
+ # Try database first (faster)
175
+ stored_track = self.track_database.get_track_by_id(media_server_rating_key=track_id)
176
+ if stored_track:
177
+ return stored_track.to_track()
178
+
179
+ # Fallback to media server API
180
+ return self.media_server_repository.get_track_by_id(track_id)
181
+
182
+ def get_all_tracks(self, limit: Optional[int] = None, offset: int = 0) -> List[Track]:
183
+ """Get all tracks from the database with optional pagination."""
184
+ stored_tracks = self.track_database.get_all_tracks(limit=limit, offset=offset)
185
+ return [stored_track.to_track() for stored_track in stored_tracks]
186
+
187
+ def search_tracks_in_database(self, search_query: str, limit: Optional[int] = None, offset: int = 0) -> List[Track]:
188
+ """Search tracks in the database by artist, album, or title."""
189
+ stored_tracks = self.track_database.search_tracks(search_query, limit=limit, offset=offset)
190
+ return [stored_track.to_track() for stored_track in stored_tracks]
191
+
192
+ def count_tracks_in_database(self, search_query: str) -> int:
193
+ """Count tracks matching search query in the database."""
194
+ return self.track_database.count_search_tracks(search_query)
195
+
196
+ def search_tracks_advanced(
197
+ self,
198
+ artist: Optional[str] = None,
199
+ album: Optional[str] = None,
200
+ title: Optional[str] = None,
201
+ limit: Optional[int] = None,
202
+ offset: int = 0
203
+ ) -> List[Track]:
204
+ """Search tracks in the database using advanced criteria with AND logic."""
205
+ stored_tracks = self.track_database.search_tracks_advanced(
206
+ artist=artist, album=album, title=title, limit=limit, offset=offset
207
+ )
208
+ return [stored_track.to_track() for stored_track in stored_tracks]
209
+
210
+ def count_tracks_advanced(
211
+ self,
212
+ artist: Optional[str] = None,
213
+ album: Optional[str] = None,
214
+ title: Optional[str] = None
215
+ ) -> int:
216
+ """Count tracks matching advanced search criteria in the database."""
217
+ return self.track_database.count_search_tracks_advanced(artist=artist, album=album, title=title)
218
+
219
+ def has_embedding(self, track_id: str) -> bool:
220
+ """Check if embedding exists for a track."""
221
+ return self.embedding_repository.has_embedding(track_id)
222
+
223
+ def save_embedding(self, track_id: str, embedding: List[float]) -> None:
224
+ """Save an embedding for a track."""
225
+ # Get track info first
226
+ track = self.get_track_by_id(track_id=track_id)
227
+ if track:
228
+ track_embedding = TrackEmbedding(
229
+ track=track,
230
+ embedding=embedding,
231
+ model_id=self._config.clap.model_id
232
+ )
233
+ self.embedding_repository.save_embedding(track_embedding)
234
+ # Also mark as processed in track database
235
+ self.track_database.mark_track_processed(media_server_rating_key=track_id,
236
+ model_id=self._config.clap.model_id)
237
+
238
+ def compute_single_embedding(self, audio_filepath: str) -> List[float]:
239
+ """Compute embedding for a single audio file."""
240
+ return self.embedding_generator.generate_embedding(Path(audio_filepath))
241
+
242
+ def initialize_worker_processing(self, job_queue_service, api_host: str = "localhost", api_port: int = 8000):
243
+ """Initialize worker-based processing use case."""
244
+ self.worker_processing = WorkerBasedProcessingUseCase(
245
+ job_queue_service,
246
+ self.track_database,
247
+ api_host,
248
+ api_port
249
+ )
250
+
251
+ def can_use_workers(self) -> bool:
252
+ """Check if workers are available for processing."""
253
+ if not hasattr(self, 'worker_processing'):
254
+ return False
255
+ return self.worker_processing.can_use_workers()
256
+
257
+ def get_worker_info(self) -> Dict[str, Any]:
258
+ """Get information about available workers."""
259
+ if not hasattr(self, 'worker_processing'):
260
+ return {"active_workers": 0, "worker_details": [], "queue_stats": {}}
261
+ return self.worker_processing.get_worker_info()
262
+
263
+ def create_worker_tasks(self, max_tracks: Optional[int] = None) -> Dict[str, Any]:
264
+ """Create tasks for worker processing."""
265
+ if not hasattr(self, 'worker_processing'):
266
+ return {
267
+ "success": False,
268
+ "message": "Worker processing not initialized",
269
+ "tasks_created": 0
270
+ }
271
+ return self.worker_processing.create_worker_tasks(max_tracks=max_tracks, model_id=self._config.clap.model_id)
272
+
273
+ def stop_worker_processing(self) -> Dict[str, Any]:
274
+ """Stop worker processing by clearing pending tasks."""
275
+ if not hasattr(self, 'worker_processing'):
276
+ return {"cleared_tasks": 0, "message": "Worker processing not initialized"}
277
+
278
+ # Clear pending tasks from the job queue
279
+ cleared_count = self.worker_processing.job_queue.clear_pending_tasks()
280
+
281
+ return {
282
+ "cleared_tasks": cleared_count,
283
+ "message": f"Cleared {cleared_count} pending tasks. Tasks currently being processed by workers will complete."
284
+ }
285
+
286
+ def has_active_worker_processing(self) -> bool:
287
+ """Check if there are active worker processing tasks."""
288
+ if not hasattr(self, 'worker_processing'):
289
+ return False
290
+ return self.worker_processing.job_queue.has_active_processing()
291
+
292
+ def cleanup_stale_worker_tasks(self) -> int:
293
+ """Clean up stale worker tasks and return count of cleaned tasks."""
294
+ if not hasattr(self, 'worker_processing'):
295
+ return 0
296
+ return self.worker_processing.job_queue.cleanup_stale_tasks()
297
+
298
+ def create_playlist(self, name: str, track_ids: List[str], batch_size: int = 100) -> "Playlist":
299
+ """Create a playlist from a list of track IDs.
300
+
301
+ Args:
302
+ name: Name of the playlist
303
+ track_ids: List of track IDs to include in the playlist
304
+ batch_size: Number of tracks to add per batch for large playlists (default: 100)
305
+ """
306
+ try:
307
+ # Get tracks by their IDs
308
+ tracks = []
309
+ for track_id in track_ids:
310
+ track = self.get_track_by_id(track_id=track_id)
311
+ if track:
312
+ tracks.append(track)
313
+ else:
314
+ self.logger.warning(f"Track with ID {track_id} not found, skipping")
315
+
316
+ if not tracks:
317
+ raise ValueError("No valid tracks found for playlist creation")
318
+
319
+ # Create playlist object
320
+ playlist = Playlist(name=name, tracks=tracks)
321
+
322
+ # Create playlist on the media server with batching
323
+ created_playlist = self.media_server_repository.create_playlist(playlist, batch_size=batch_size)
324
+
325
+ self.logger.info(f"Successfully created playlist '{name}' with {len(tracks)} tracks")
326
+ return created_playlist
327
+
328
+ except Exception as e:
329
+ self.logger.error(f"Failed to create playlist '{name}': {e}", exc_info=True)
330
+ raise
331
+
332
+ def cleanup(self) -> None:
333
+ """Clean up resources, including unloading the CLAP model."""
334
+ try:
335
+ self.logger.info("Cleaning up MyceliumService resources...")
336
+ if hasattr(self, 'embedding_generator') and self.embedding_generator:
337
+ self.embedding_generator.unload_model()
338
+ self.logger.info("MyceliumService cleanup completed successfully")
339
+ except Exception as e:
340
+ self.logger.error(f"Error during MyceliumService cleanup: {e}", exc_info=True)