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.
- mycelium/__init__.py +0 -0
- mycelium/api/__init__.py +0 -0
- mycelium/api/app.py +1147 -0
- mycelium/api/client_app.py +170 -0
- mycelium/api/generated_sources/__init__.py +0 -0
- mycelium/api/generated_sources/server_schemas/__init__.py +97 -0
- mycelium/api/generated_sources/server_schemas/api/__init__.py +5 -0
- mycelium/api/generated_sources/server_schemas/api/default_api.py +2473 -0
- mycelium/api/generated_sources/server_schemas/api_client.py +766 -0
- mycelium/api/generated_sources/server_schemas/api_response.py +25 -0
- mycelium/api/generated_sources/server_schemas/configuration.py +434 -0
- mycelium/api/generated_sources/server_schemas/exceptions.py +166 -0
- mycelium/api/generated_sources/server_schemas/models/__init__.py +41 -0
- mycelium/api/generated_sources/server_schemas/models/api_section.py +71 -0
- mycelium/api/generated_sources/server_schemas/models/chroma_section.py +69 -0
- mycelium/api/generated_sources/server_schemas/models/clap_section.py +75 -0
- mycelium/api/generated_sources/server_schemas/models/compute_on_server200_response.py +79 -0
- mycelium/api/generated_sources/server_schemas/models/compute_on_server_request.py +67 -0
- mycelium/api/generated_sources/server_schemas/models/compute_text_search_request.py +69 -0
- mycelium/api/generated_sources/server_schemas/models/config_request.py +81 -0
- mycelium/api/generated_sources/server_schemas/models/config_response.py +107 -0
- mycelium/api/generated_sources/server_schemas/models/create_playlist_request.py +71 -0
- mycelium/api/generated_sources/server_schemas/models/get_similar_by_track200_response.py +143 -0
- mycelium/api/generated_sources/server_schemas/models/library_stats_response.py +77 -0
- mycelium/api/generated_sources/server_schemas/models/logging_section.py +67 -0
- mycelium/api/generated_sources/server_schemas/models/media_server_section.py +67 -0
- mycelium/api/generated_sources/server_schemas/models/playlist_response.py +73 -0
- mycelium/api/generated_sources/server_schemas/models/plex_section.py +71 -0
- mycelium/api/generated_sources/server_schemas/models/processing_response.py +90 -0
- mycelium/api/generated_sources/server_schemas/models/save_config_response.py +73 -0
- mycelium/api/generated_sources/server_schemas/models/scan_library_response.py +75 -0
- mycelium/api/generated_sources/server_schemas/models/search_result_response.py +75 -0
- mycelium/api/generated_sources/server_schemas/models/server_section.py +67 -0
- mycelium/api/generated_sources/server_schemas/models/stop_processing_response.py +71 -0
- mycelium/api/generated_sources/server_schemas/models/task_status_response.py +87 -0
- mycelium/api/generated_sources/server_schemas/models/track_database_stats.py +75 -0
- mycelium/api/generated_sources/server_schemas/models/track_response.py +77 -0
- mycelium/api/generated_sources/server_schemas/models/tracks_list_response.py +81 -0
- mycelium/api/generated_sources/server_schemas/rest.py +329 -0
- mycelium/api/generated_sources/server_schemas/test/__init__.py +0 -0
- mycelium/api/generated_sources/server_schemas/test/test_api_section.py +57 -0
- mycelium/api/generated_sources/server_schemas/test/test_chroma_section.py +55 -0
- mycelium/api/generated_sources/server_schemas/test/test_clap_section.py +60 -0
- mycelium/api/generated_sources/server_schemas/test/test_compute_on_server200_response.py +52 -0
- mycelium/api/generated_sources/server_schemas/test/test_compute_on_server_request.py +53 -0
- mycelium/api/generated_sources/server_schemas/test/test_compute_text_search_request.py +54 -0
- mycelium/api/generated_sources/server_schemas/test/test_config_request.py +66 -0
- mycelium/api/generated_sources/server_schemas/test/test_config_response.py +97 -0
- mycelium/api/generated_sources/server_schemas/test/test_create_playlist_request.py +60 -0
- mycelium/api/generated_sources/server_schemas/test/test_default_api.py +150 -0
- mycelium/api/generated_sources/server_schemas/test/test_get_similar_by_track200_response.py +61 -0
- mycelium/api/generated_sources/server_schemas/test/test_library_stats_response.py +63 -0
- mycelium/api/generated_sources/server_schemas/test/test_logging_section.py +53 -0
- mycelium/api/generated_sources/server_schemas/test/test_media_server_section.py +53 -0
- mycelium/api/generated_sources/server_schemas/test/test_playlist_response.py +58 -0
- mycelium/api/generated_sources/server_schemas/test/test_plex_section.py +56 -0
- mycelium/api/generated_sources/server_schemas/test/test_processing_response.py +61 -0
- mycelium/api/generated_sources/server_schemas/test/test_save_config_response.py +58 -0
- mycelium/api/generated_sources/server_schemas/test/test_scan_library_response.py +61 -0
- mycelium/api/generated_sources/server_schemas/test/test_search_result_response.py +69 -0
- mycelium/api/generated_sources/server_schemas/test/test_server_section.py +53 -0
- mycelium/api/generated_sources/server_schemas/test/test_stop_processing_response.py +55 -0
- mycelium/api/generated_sources/server_schemas/test/test_task_status_response.py +71 -0
- mycelium/api/generated_sources/server_schemas/test/test_track_database_stats.py +60 -0
- mycelium/api/generated_sources/server_schemas/test/test_track_response.py +63 -0
- mycelium/api/generated_sources/server_schemas/test/test_tracks_list_response.py +75 -0
- mycelium/api/generated_sources/worker_schemas/__init__.py +61 -0
- mycelium/api/generated_sources/worker_schemas/api/__init__.py +5 -0
- mycelium/api/generated_sources/worker_schemas/api/default_api.py +318 -0
- mycelium/api/generated_sources/worker_schemas/api_client.py +766 -0
- mycelium/api/generated_sources/worker_schemas/api_response.py +25 -0
- mycelium/api/generated_sources/worker_schemas/configuration.py +434 -0
- mycelium/api/generated_sources/worker_schemas/exceptions.py +166 -0
- mycelium/api/generated_sources/worker_schemas/models/__init__.py +23 -0
- mycelium/api/generated_sources/worker_schemas/models/save_config_response.py +73 -0
- mycelium/api/generated_sources/worker_schemas/models/worker_clap_section.py +75 -0
- mycelium/api/generated_sources/worker_schemas/models/worker_client_api_section.py +69 -0
- mycelium/api/generated_sources/worker_schemas/models/worker_client_section.py +79 -0
- mycelium/api/generated_sources/worker_schemas/models/worker_config_request.py +73 -0
- mycelium/api/generated_sources/worker_schemas/models/worker_config_response.py +89 -0
- mycelium/api/generated_sources/worker_schemas/models/worker_logging_section.py +67 -0
- mycelium/api/generated_sources/worker_schemas/rest.py +329 -0
- mycelium/api/generated_sources/worker_schemas/test/__init__.py +0 -0
- mycelium/api/generated_sources/worker_schemas/test/test_default_api.py +45 -0
- mycelium/api/generated_sources/worker_schemas/test/test_save_config_response.py +58 -0
- mycelium/api/generated_sources/worker_schemas/test/test_worker_clap_section.py +60 -0
- mycelium/api/generated_sources/worker_schemas/test/test_worker_client_api_section.py +55 -0
- mycelium/api/generated_sources/worker_schemas/test/test_worker_client_section.py +65 -0
- mycelium/api/generated_sources/worker_schemas/test/test_worker_config_request.py +59 -0
- mycelium/api/generated_sources/worker_schemas/test/test_worker_config_response.py +89 -0
- mycelium/api/generated_sources/worker_schemas/test/test_worker_logging_section.py +53 -0
- mycelium/api/worker_models.py +99 -0
- mycelium/application/__init__.py +11 -0
- mycelium/application/job_queue.py +323 -0
- mycelium/application/library_management_use_cases.py +292 -0
- mycelium/application/search_use_cases.py +96 -0
- mycelium/application/services.py +340 -0
- mycelium/client.py +554 -0
- mycelium/client_config.py +251 -0
- mycelium/client_frontend_dist/404.html +1 -0
- mycelium/client_frontend_dist/_next/static/a4iyRdfsvkjdyMAK9cE9Y/_buildManifest.js +1 -0
- mycelium/client_frontend_dist/_next/static/a4iyRdfsvkjdyMAK9cE9Y/_ssgManifest.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/964-830f77d7ce1c2463.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/app/_not-found/page-d25eede5a9099bd3.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/app/layout-9b3d32f96dfe13b6.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/app/page-cc6bad295789134e.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/framework-7c95b8e5103c9e90.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/main-6b37be50736577a2.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/main-app-4153d115599d3126.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/pages/_app-0a0020ddd67f79cf.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/pages/_error-03529f2c21436739.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- mycelium/client_frontend_dist/_next/static/chunks/webpack-c81e624915b2ea70.js +1 -0
- mycelium/client_frontend_dist/_next/static/css/1eb7f0e2c78e0734.css +1 -0
- mycelium/client_frontend_dist/favicon.ico +0 -0
- mycelium/client_frontend_dist/file.svg +1 -0
- mycelium/client_frontend_dist/globe.svg +1 -0
- mycelium/client_frontend_dist/index.html +1 -0
- mycelium/client_frontend_dist/index.txt +20 -0
- mycelium/client_frontend_dist/next.svg +1 -0
- mycelium/client_frontend_dist/vercel.svg +1 -0
- mycelium/client_frontend_dist/window.svg +1 -0
- mycelium/config.py +346 -0
- mycelium/domain/__init__.py +13 -0
- mycelium/domain/models.py +71 -0
- mycelium/domain/repositories.py +98 -0
- mycelium/domain/worker.py +77 -0
- mycelium/frontend_dist/404.html +1 -0
- mycelium/frontend_dist/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/964-830f77d7ce1c2463.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/app/_not-found/page-d25eede5a9099bd3.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/app/layout-9b3d32f96dfe13b6.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/app/page-a761463485e0540b.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/framework-7c95b8e5103c9e90.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/main-6b37be50736577a2.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/main-app-4153d115599d3126.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/pages/_app-0a0020ddd67f79cf.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/pages/_error-03529f2c21436739.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- mycelium/frontend_dist/_next/static/chunks/webpack-c81e624915b2ea70.js +1 -0
- mycelium/frontend_dist/_next/static/css/1eb7f0e2c78e0734.css +1 -0
- mycelium/frontend_dist/_next/static/glVJ0yJSL0zWN7anTTG3_/_buildManifest.js +1 -0
- mycelium/frontend_dist/_next/static/glVJ0yJSL0zWN7anTTG3_/_ssgManifest.js +1 -0
- mycelium/frontend_dist/favicon.ico +0 -0
- mycelium/frontend_dist/file.svg +1 -0
- mycelium/frontend_dist/globe.svg +1 -0
- mycelium/frontend_dist/index.html +10 -0
- mycelium/frontend_dist/index.txt +20 -0
- mycelium/frontend_dist/next.svg +1 -0
- mycelium/frontend_dist/vercel.svg +1 -0
- mycelium/frontend_dist/window.svg +1 -0
- mycelium/infrastructure/__init__.py +17 -0
- mycelium/infrastructure/chroma_adapter.py +232 -0
- mycelium/infrastructure/clap_adapter.py +280 -0
- mycelium/infrastructure/plex_adapter.py +145 -0
- mycelium/infrastructure/track_database.py +467 -0
- mycelium/main.py +183 -0
- mycelium_ai-0.5.0.dist-info/METADATA +312 -0
- mycelium_ai-0.5.0.dist-info/RECORD +164 -0
- mycelium_ai-0.5.0.dist-info/WHEEL +5 -0
- mycelium_ai-0.5.0.dist-info/entry_points.txt +2 -0
- mycelium_ai-0.5.0.dist-info/licenses/LICENSE +21 -0
- 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)
|