From 1af4fd3df6da48e0ad51c3af51282c7c5e3c7540 Mon Sep 17 00:00:00 2001 From: mazemaze Date: Sat, 20 Dec 2025 23:08:57 +0900 Subject: [PATCH] Deploy update --- .claude/settings.json | 10 +- .workflow/index.yml | 11 +- .../versions/v005/contracts/api_contract.yml | 954 ++++++++++++ .../versions/v005/design/design_document.yml | 569 +++++++ .../versions/v005/requirements/expanded.yml | 117 ++ .../versions/v005/requirements/final.yml | 90 ++ .workflow/versions/v005/session.yml | 30 + .workflow/versions/v005/session.yml.bak | 30 + .../v005/snapshot_after/manifest.json | 1360 +++++++++++++++++ .../v005/snapshot_before/manifest.json | 737 +++++++++ app/api/auth/confirm-email/route.ts | 75 + app/api/auth/logout/route.ts | 44 + app/api/auth/refresh/route.ts | 67 + app/api/auth/sessions/[id]/route.ts | 53 + app/api/auth/sessions/route.ts | 31 + app/api/auth/verify-email/route.ts | 70 + app/api/player/history/route.ts | 93 ++ app/api/player/next/route.ts | 36 + app/api/player/pause/route.ts | 31 + app/api/player/play/route.ts | 68 + app/api/player/previous/route.ts | 36 + app/api/player/queue/route.ts | 161 ++ app/api/search/index/route.ts | 79 + app/api/search/route.ts | 169 +- app/api/search/suggestions/route.ts | 41 + app/api/share/[token]/click/route.ts | 26 - app/api/share/[token]/route.ts | 104 -- app/api/share/album/[id]/route.ts | 53 - app/api/share/playlist/[id]/route.ts | 53 - app/api/share/song/[id]/route.ts | 53 - .../chunk/[uploadId]/[chunkIndex]/route.ts | 105 ++ app/api/upload/complete/[uploadId]/route.ts | 112 ++ app/api/upload/init/route.ts | 79 + app/api/upload/presigned-url/route.ts | 45 + app/components/AudioPlayer.tsx | 404 +++++ app/components/EmailVerification.tsx | 344 +++++ app/components/SearchBar.tsx | 359 +++++ app/components/SearchResults.tsx | 530 +++++++ app/components/SessionManager.tsx | 294 ++++ app/components/TokenRefreshManager.tsx | 315 ++++ app/components/UploadManager.tsx | 507 ++++++ app/s/[token]/page.tsx | 88 -- app/types/api.ts | 167 ++ lib/auth.ts | 168 +- lib/player.ts | 253 +++ lib/search.ts | 287 ++++ lib/upload.ts | 160 ++ prisma/schema.prisma | 139 +- project_manifest.json | 697 ++++++++- 49 files changed, 9767 insertions(+), 537 deletions(-) create mode 100644 .workflow/versions/v005/contracts/api_contract.yml create mode 100644 .workflow/versions/v005/design/design_document.yml create mode 100644 .workflow/versions/v005/requirements/expanded.yml create mode 100644 .workflow/versions/v005/requirements/final.yml create mode 100644 .workflow/versions/v005/session.yml create mode 100644 .workflow/versions/v005/session.yml.bak create mode 100644 .workflow/versions/v005/snapshot_after/manifest.json create mode 100644 .workflow/versions/v005/snapshot_before/manifest.json create mode 100644 app/api/auth/confirm-email/route.ts create mode 100644 app/api/auth/logout/route.ts create mode 100644 app/api/auth/refresh/route.ts create mode 100644 app/api/auth/sessions/[id]/route.ts create mode 100644 app/api/auth/sessions/route.ts create mode 100644 app/api/auth/verify-email/route.ts create mode 100644 app/api/player/history/route.ts create mode 100644 app/api/player/next/route.ts create mode 100644 app/api/player/pause/route.ts create mode 100644 app/api/player/play/route.ts create mode 100644 app/api/player/previous/route.ts create mode 100644 app/api/player/queue/route.ts create mode 100644 app/api/search/index/route.ts create mode 100644 app/api/search/suggestions/route.ts delete mode 100644 app/api/share/[token]/click/route.ts delete mode 100644 app/api/share/[token]/route.ts delete mode 100644 app/api/share/album/[id]/route.ts delete mode 100644 app/api/share/playlist/[id]/route.ts delete mode 100644 app/api/share/song/[id]/route.ts create mode 100644 app/api/upload/chunk/[uploadId]/[chunkIndex]/route.ts create mode 100644 app/api/upload/complete/[uploadId]/route.ts create mode 100644 app/api/upload/init/route.ts create mode 100644 app/api/upload/presigned-url/route.ts create mode 100644 app/components/AudioPlayer.tsx create mode 100644 app/components/EmailVerification.tsx create mode 100644 app/components/SearchBar.tsx create mode 100644 app/components/SearchResults.tsx create mode 100644 app/components/SessionManager.tsx create mode 100644 app/components/TokenRefreshManager.tsx create mode 100644 app/components/UploadManager.tsx delete mode 100644 app/s/[token]/page.tsx create mode 100644 app/types/api.ts create mode 100644 lib/player.ts create mode 100644 lib/search.ts create mode 100644 lib/upload.ts diff --git a/.claude/settings.json b/.claude/settings.json index 8ab61e4..18271c8 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -221,5 +221,13 @@ ] } ] - } + }, + "env": { + "ANTHROPIC_AUTH_TOKEN": "2ae0d935516f4a3f9d10957712af54f5.5Up9iyO5Y04If7Vk", + "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic", + "API_TIMEOUT_MS": "3000000", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.5-air", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6" + } } diff --git a/.workflow/index.yml b/.workflow/index.yml index 71baba8..3313339 100644 --- a/.workflow/index.yml +++ b/.workflow/index.yml @@ -27,5 +27,12 @@ versions: completed_at: '2025-12-18T18:24:29.951800' tasks_count: 0 operations_count: 0 -latest_version: v004 -total_versions: 4 +- version: v005 + feature: examine what is missing in current app and implement it + status: completed + started_at: '2025-12-20T22:02:10.309550' + completed_at: '2025-12-20T22:28:45.494863' + tasks_count: 0 + operations_count: 0 +latest_version: v005 +total_versions: 5 diff --git a/.workflow/versions/v005/contracts/api_contract.yml b/.workflow/versions/v005/contracts/api_contract.yml new file mode 100644 index 0000000..3d1723a --- /dev/null +++ b/.workflow/versions/v005/contracts/api_contract.yml @@ -0,0 +1,954 @@ +api_contract: + workflow_version: v005 + design_document_revision: 1 + generated_at: '2025-12-20T22:10:36.422275' + validated_at: null + status: draft +types: +- id: type_RefreshToken + name: RefreshToken + definition: + type: object + properties: + - name: id + type: string + required: true + description: Unique identifier + - name: token + type: string + required: true + description: Refresh token value + - name: userId + type: string + required: true + description: User who owns this token + - name: expiresAt + type: Date + required: true + description: Token expiration time + - name: createdAt + type: Date + required: true + description: When token was created + - name: isRevoked + type: boolean + required: false + description: Whether token has been revoked + used_by: + models: + - model_refresh_token + responses: [] + requests: [] +- id: type_Session + name: Session + definition: + type: object + properties: + - name: id + type: string + required: true + description: Unique session identifier + - name: userId + type: string + required: true + description: User who owns this session + - name: token + type: string + required: true + description: Session token + - name: deviceInfo + type: Record + required: false + description: Device information (browser, OS) + - name: ipAddress + type: string + required: false + description: Client IP address + - name: userAgent + type: string + required: false + description: Browser user agent + - name: lastActivity + type: Date + required: false + description: Last activity timestamp + - name: createdAt + type: Date + required: true + description: When session was created + used_by: + models: + - model_session + responses: [] + requests: [] +- id: type_PlayHistory + name: PlayHistory + definition: + type: object + properties: + - name: id + type: string + required: true + description: Unique identifier + - name: userId + type: string + required: true + description: User who played the song + - name: songId + type: string + required: true + description: Song that was played + - name: playedAt + type: Date + required: true + description: When playback started + - name: playedDuration + type: number + required: false + description: Seconds actually played + - name: completed + type: boolean + required: false + description: Did user listen to completion + - name: source + type: string + required: false + description: Where playback was initiated + used_by: + models: + - model_play_history + responses: [] + requests: [] +- id: type_Queue + name: Queue + definition: + type: object + properties: + - name: id + type: string + required: true + description: Unique queue identifier + - name: userId + type: string + required: true + description: User who owns this queue + - name: songIds + type: Record + required: true + description: Array of song IDs in order + - name: currentIndex + type: number + required: false + description: Current playing index + - name: isShuffled + type: boolean + required: false + description: Whether queue is shuffled + - name: repeatMode + type: string + required: false + description: 'Repeat mode: none, one, all' + - name: createdAt + type: Date + required: true + description: When queue was created + - name: updatedAt + type: Date + required: true + description: Last update time + used_by: + models: + - model_queue + responses: [] + requests: [] +- id: type_UploadSession + name: UploadSession + definition: + type: object + properties: + - name: id + type: string + required: true + description: Unique upload session ID + - name: userId + type: string + required: true + description: User uploading the file + - name: fileName + type: string + required: true + description: Original file name + - name: fileSize + type: number + required: true + description: Total file size in bytes + - name: mimeType + type: string + required: true + description: File MIME type + - name: chunkSize + type: number + required: true + description: Size of each chunk + - name: totalChunks + type: number + required: true + description: Total number of chunks + - name: uploadedChunks + type: Record + required: false + description: Array of uploaded chunk numbers + - name: status + type: string + required: false + description: Upload status + - name: fileId + type: string + required: false + description: Associated file ID when complete + - name: metadata + type: Record + required: false + description: Additional file metadata + - name: createdAt + type: Date + required: true + description: When upload started + - name: expiresAt + type: Date + required: true + description: When upload session expires + used_by: + models: + - model_upload_session + responses: [] + requests: [] +- id: type_SearchIndex + name: SearchIndex + definition: + type: object + properties: + - name: id + type: string + required: true + description: Unique index entry ID + - name: entityType + type: string + required: true + description: Type of entity (song, album, artist) + - name: entityId + type: string + required: true + description: ID of the indexed entity + - name: title + type: string + required: true + description: Entity title for search + - name: content + type: string + required: false + description: Full text content + - name: metadata + type: Record + required: false + description: Additional searchable metadata + - name: createdAt + type: Date + required: true + description: When indexed + - name: updatedAt + type: Date + required: true + description: Last update + used_by: + models: + - model_search_index + responses: [] + requests: [] +endpoints: +- id: api_auth_refresh + method: POST + path: /api/auth/refresh + path_params: [] + query_params: [] + request_body: + type_id: type_AuthRefreshRequest + content_type: application/json + response: + success: + status: 200 + type_id: type_AuthRefreshResponse + is_array: false + errors: + - status: 401 + type_id: type_ApiError + description: Invalid or expired refresh token + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_auth_logout + method: POST + path: /api/auth/logout + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_auth_sessions + method: GET + path: /api/auth/sessions + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_auth_revoke_session + method: DELETE + path: /api/auth/sessions/:id + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_auth_verify_email + method: POST + path: /api/auth/verify-email + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_auth_confirm_email + method: POST + path: /api/auth/confirm-email + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_play + method: POST + path: /api/player/play + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_pause + method: POST + path: /api/player/pause + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_next + method: POST + path: /api/player/next + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_previous + method: POST + path: /api/player/previous + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_queue + method: GET + path: /api/player/queue + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_queue_add + method: POST + path: /api/player/queue + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_queue_clear + method: DELETE + path: /api/player/queue + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_player_history + method: GET + path: /api/player/history + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_upload_init + method: POST + path: /api/upload/init + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_upload_chunk + method: POST + path: /api/upload/chunk/:uploadId/:chunkIndex + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_upload_complete + method: POST + path: /api/upload/complete/:uploadId + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_upload_presigned + method: GET + path: /api/upload/presigned-url + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_search + method: GET + path: /api/search + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_search_suggestions + method: GET + path: /api/search/suggestions + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +- id: api_search_index + method: POST + path: /api/search/index + path_params: [] + query_params: [] + request_body: null + response: + success: + status: 200 + type_id: null + is_array: false + errors: [] + auth: + required: false + roles: [] + version: 1.0.0 +frontend_calls: +- id: call_page_settings_security_api_auth_sessions + source: + entity_id: page_settings_security + file_path: app/settings/security/page.tsx + endpoint_id: api_auth_sessions + purpose: Load active sessions + trigger: onLoad + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Update state + error_action: Show error +- id: call_component_audio_player_api_player_play + source: + entity_id: component_audio_player + file_path: app/components/AudioPlayer.tsx + endpoint_id: api_player_play + purpose: Call api_player_play + trigger: onSubmit + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_audio_player_api_player_pause + source: + entity_id: component_audio_player + file_path: app/components/AudioPlayer.tsx + endpoint_id: api_player_pause + purpose: Call api_player_pause + trigger: onSubmit + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_audio_player_api_player_next + source: + entity_id: component_audio_player + file_path: app/components/AudioPlayer.tsx + endpoint_id: api_player_next + purpose: Call api_player_next + trigger: onSubmit + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_audio_player_api_player_previous + source: + entity_id: component_audio_player + file_path: app/components/AudioPlayer.tsx + endpoint_id: api_player_previous + purpose: Call api_player_previous + trigger: onSubmit + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_audio_player_api_player_queue + source: + entity_id: component_audio_player + file_path: app/components/AudioPlayer.tsx + endpoint_id: api_player_queue + purpose: Call api_player_queue + trigger: onDemand + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_upload_manager_api_upload_init + source: + entity_id: component_upload_manager + file_path: app/components/UploadManager.tsx + endpoint_id: api_upload_init + purpose: Call api_upload_init + trigger: onSubmit + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_upload_manager_api_upload_chunk + source: + entity_id: component_upload_manager + file_path: app/components/UploadManager.tsx + endpoint_id: api_upload_chunk + purpose: Call api_upload_chunk + trigger: onSubmit + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_upload_manager_api_upload_complete + source: + entity_id: component_upload_manager + file_path: app/components/UploadManager.tsx + endpoint_id: api_upload_complete + purpose: Call api_upload_complete + trigger: onSubmit + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_search_bar_api_search_suggestions + source: + entity_id: component_search_bar + file_path: app/components/SearchBar.tsx + endpoint_id: api_search_suggestions + purpose: Call api_search_suggestions + trigger: onDemand + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +- id: call_component_search_results_api_search + source: + entity_id: component_search_results + file_path: app/components/SearchResults.tsx + endpoint_id: api_search + purpose: Call api_search + trigger: onDemand + request_mapping: + from_props: [] + from_state: [] + from_form: [] + response_handling: + success_action: Handle response + error_action: Show error +backend_routes: +- id: route_post_auth_refresh + endpoint_id: api_auth_refresh + file_path: app/api/auth/refresh/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_auth_logout + endpoint_id: api_auth_logout + file_path: app/api/auth/logout/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_get_auth_sessions + endpoint_id: api_auth_sessions + file_path: app/api/auth/sessions/route.ts + export_name: GET + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_delete_auth_sessions_:id + endpoint_id: api_auth_revoke_session + file_path: app/api/auth/sessions/[id]/route.ts + export_name: DELETE + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_auth_verify-email + endpoint_id: api_auth_verify_email + file_path: app/api/auth/verify-email/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_auth_confirm-email + endpoint_id: api_auth_confirm_email + file_path: app/api/auth/confirm-email/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_player_play + endpoint_id: api_player_play + file_path: app/api/player/play/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_player_pause + endpoint_id: api_player_pause + file_path: app/api/player/pause/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_player_next + endpoint_id: api_player_next + file_path: app/api/player/next/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_player_previous + endpoint_id: api_player_previous + file_path: app/api/player/previous/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_get_player_queue + endpoint_id: api_player_queue + file_path: app/api/player/queue/route.ts + export_name: GET + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_player_queue + endpoint_id: api_player_queue_add + file_path: app/api/player/queue/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_delete_player_queue + endpoint_id: api_player_queue_clear + file_path: app/api/player/queue/route.ts + export_name: DELETE + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_get_player_history + endpoint_id: api_player_history + file_path: app/api/player/history/route.ts + export_name: GET + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_upload_init + endpoint_id: api_upload_init + file_path: app/api/upload/init/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_upload_chunk_:uploadId_:chunkIndex + endpoint_id: api_upload_chunk + file_path: app/api/upload/chunk/[uploadId]/[chunkIndex]/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_upload_complete_:uploadId + endpoint_id: api_upload_complete + file_path: app/api/upload/complete/[uploadId]/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_get_upload_presigned-url + endpoint_id: api_upload_presigned + file_path: app/api/upload/presigned-url/route.ts + export_name: GET + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_get_search + endpoint_id: api_search + file_path: app/api/search/route.ts + export_name: GET + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_get_search_suggestions + endpoint_id: api_search_suggestions + file_path: app/api/search/suggestions/route.ts + export_name: GET + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] +- id: route_post_search_index + endpoint_id: api_search_index + file_path: app/api/search/index/route.ts + export_name: POST + uses_models: [] + uses_services: [] + must_validate: [] + must_authenticate: false + must_authorize: [] diff --git a/.workflow/versions/v005/design/design_document.yml b/.workflow/versions/v005/design/design_document.yml new file mode 100644 index 0000000..54610f0 --- /dev/null +++ b/.workflow/versions/v005/design/design_document.yml @@ -0,0 +1,569 @@ +workflow_version: "v005" +feature: "examine what is missing in current app and implement it" +created_at: "2025-12-20T22:05:00Z" +status: "draft" +revision: 1 + +data_models: + # New RefreshToken model for token rotation + - id: model_refresh_token + name: RefreshToken + description: "JWT refresh tokens for secure authentication" + table_name: refresh_tokens + fields: + - name: id + type: string + constraints: ["primary_key", "default(cuid())"] + description: "Unique identifier" + - name: token + type: string + constraints: ["unique", "not_null"] + description: "Refresh token value" + - name: userId + type: string + constraints: ["not_null", "indexed"] + description: "User who owns this token" + - name: expiresAt + type: datetime + constraints: ["not_null", "indexed"] + description: "Token expiration time" + - name: createdAt + type: datetime + constraints: ["not_null", "default(now())"] + description: "When token was created" + - name: isRevoked + type: boolean + constraints: ["default(false)", "indexed"] + description: "Whether token has been revoked" + relations: + - type: belongs_to + target: model_user + foreign_key: userId + on_delete: cascade + timestamps: false + + # New Session model for active session tracking + - id: model_session + name: Session + description: "Active user sessions with device tracking" + table_name: sessions + fields: + - name: id + type: string + constraints: ["primary_key", "default(cuid())"] + description: "Unique session identifier" + - name: userId + type: string + constraints: ["not_null", "indexed"] + description: "User who owns this session" + - name: token + type: string + constraints: ["unique", "not_null"] + description: "Session token" + - name: deviceInfo + type: json + constraints: [] + description: "Device information (browser, OS)" + - name: ipAddress + type: string + constraints: [] + description: "Client IP address" + - name: userAgent + type: string + constraints: [] + description: "Browser user agent" + - name: lastActivity + type: datetime + constraints: ["default(now())"] + description: "Last activity timestamp" + - name: createdAt + type: datetime + constraints: ["not_null", "default(now())"] + description: "When session was created" + relations: + - type: belongs_to + target: model_user + foreign_key: userId + relation_name: UserSessions + on_delete: cascade + timestamps: false + + # New PlayHistory model + - id: model_play_history + name: PlayHistory + description: "Track user play history for analytics" + table_name: play_history + fields: + - name: id + type: string + constraints: ["primary_key", "default(cuid())"] + description: "Unique identifier" + - name: userId + type: string + constraints: ["not_null", "indexed"] + description: "User who played the song" + - name: songId + type: string + constraints: ["not_null", "indexed"] + description: "Song that was played" + - name: playedAt + type: datetime + constraints: ["not_null", "default(now())", "indexed"] + description: "When playback started" + - name: playedDuration + type: integer + constraints: [] + description: "Seconds actually played" + - name: completed + type: boolean + constraints: ["default(false)"] + description: "Did user listen to completion" + - name: source + type: string + constraints: [] + description: "Where playback was initiated" + relations: + - type: belongs_to + target: model_user + foreign_key: userId + relation_name: UserPlayHistory + on_delete: cascade + - type: belongs_to + target: model_song + foreign_key: songId + relation_name: SongPlayHistory + on_delete: cascade + timestamps: false + + # New Queue model for persistent queues + - id: model_queue + name: Queue + description: "User playback queue with state" + table_name: queues + fields: + - name: id + type: string + constraints: ["primary_key", "default(cuid())"] + description: "Unique queue identifier" + - name: userId + type: string + constraints: ["not_null", "unique"] + description: "User who owns this queue" + - name: songIds + type: json + constraints: ["not_null"] + description: "Array of song IDs in order" + - name: currentIndex + type: integer + constraints: ["default(0)"] + description: "Current playing index" + - name: isShuffled + type: boolean + constraints: ["default(false)"] + description: "Whether queue is shuffled" + - name: repeatMode + type: string + constraints: ["default('none')"] + description: "Repeat mode: none, one, all" + - name: createdAt + type: datetime + constraints: ["not_null", "default(now())"] + description: "When queue was created" + - name: updatedAt + type: datetime + constraints: ["not_null", "default(now())"] + description: "Last update time" + relations: + - type: belongs_to + target: model_user + foreign_key: userId + relation_name: UserQueue + on_delete: cascade + timestamps: false + + # New UploadSession model for chunked uploads + - id: model_upload_session + name: UploadSession + description: "Chunked upload session tracking" + table_name: upload_sessions + fields: + - name: id + type: string + constraints: ["primary_key", "default(cuid())"] + description: "Unique upload session ID" + - name: userId + type: string + constraints: ["not_null", "indexed"] + description: "User uploading the file" + - name: fileName + type: string + constraints: ["not_null"] + description: "Original file name" + - name: fileSize + type: integer + constraints: ["not_null"] + description: "Total file size in bytes" + - name: mimeType + type: string + constraints: ["not_null"] + description: "File MIME type" + - name: chunkSize + type: integer + constraints: ["not_null"] + description: "Size of each chunk" + - name: totalChunks + type: integer + constraints: ["not_null"] + description: "Total number of chunks" + - name: uploadedChunks + type: json + constraints: [] + description: "Array of uploaded chunk numbers" + - name: status + type: string + constraints: ["default('pending')"] + description: "Upload status" + - name: fileId + type: string + constraints: [] + description: "Associated file ID when complete" + - name: metadata + type: json + constraints: [] + description: "Additional file metadata" + - name: createdAt + type: datetime + constraints: ["not_null", "default(now())"] + description: "When upload started" + - name: expiresAt + type: datetime + constraints: ["not_null"] + description: "When upload session expires" + relations: + - type: belongs_to + target: model_user + foreign_key: userId + on_delete: cascade + timestamps: false + + # New SearchIndex model + - id: model_search_index + name: SearchIndex + description: "Full-text search index for entities" + table_name: search_index + fields: + - name: id + type: string + constraints: ["primary_key", "default(cuid())"] + description: "Unique index entry ID" + - name: entityType + type: string + constraints: ["not_null", "indexed"] + description: "Type of entity (song, album, artist)" + - name: entityId + type: string + constraints: ["not_null", "indexed"] + description: "ID of the indexed entity" + - name: title + type: string + constraints: ["not_null"] + description: "Entity title for search" + - name: content + type: text + constraints: [] + description: "Full text content" + - name: metadata + type: json + constraints: [] + description: "Additional searchable metadata" + - name: createdAt + type: datetime + constraints: ["not_null", "default(now())"] + description: "When indexed" + - name: updatedAt + type: datetime + constraints: ["not_null", "default(now())"] + description: "Last update" + indexes: + - fields: ["entityType", "entityId"] + unique: true + name: idx_search_entity + timestamps: false + +api_endpoints: + # Session Management + - id: api_auth_refresh + method: POST + path: /api/auth/refresh + description: "Refresh access token using refresh token" + auth_required: false + request_body: + refreshToken: string + responses: + - status: 200 + description: "Token refreshed successfully" + - status: 401 + description: "Invalid or expired refresh token" + + - id: api_auth_logout + method: POST + path: /api/auth/logout + description: "Logout user and invalidate tokens" + auth_required: true + + - id: api_auth_sessions + method: GET + path: /api/auth/sessions + description: "List all active sessions" + auth_required: true + + - id: api_auth_revoke_session + method: DELETE + path: /api/auth/sessions/:id + description: "Revoke specific session" + auth_required: true + + # Email Verification + - id: api_auth_verify_email + method: POST + path: /api/auth/verify-email + description: "Send email verification" + auth_required: true + + - id: api_auth_confirm_email + method: POST + path: /api/auth/confirm-email + description: "Confirm email with token" + auth_required: false + + # Audio Player API + - id: api_player_play + method: POST + path: /api/player/play + description: "Start or resume playback" + auth_required: true + + - id: api_player_pause + method: POST + path: /api/player/pause + description: "Pause playback" + auth_required: true + + - id: api_player_next + method: POST + path: /api/player/next + description: "Skip to next track" + auth_required: true + + - id: api_player_previous + method: POST + path: /api/player/previous + description: "Go to previous track" + auth_required: true + + - id: api_player_queue + method: GET + path: /api/player/queue + description: "Get current queue" + auth_required: true + + - id: api_player_queue_add + method: POST + path: /api/player/queue + description: "Add songs to queue" + auth_required: true + + - id: api_player_queue_clear + method: DELETE + path: /api/player/queue + description: "Clear queue" + auth_required: true + + - id: api_player_history + method: GET + path: /api/player/history + description: "Get play history" + auth_required: true + + # File Upload API + - id: api_upload_init + method: POST + path: /api/upload/init + description: "Initialize chunked upload" + auth_required: true + + - id: api_upload_chunk + method: POST + path: /api/upload/chunk/:uploadId/:chunkIndex + description: "Upload chunk" + auth_required: true + + - id: api_upload_complete + method: POST + path: /api/upload/complete/:uploadId + description: "Complete upload" + auth_required: true + + - id: api_upload_presigned + method: GET + path: /api/upload/presigned-url + description: "Get presigned URL for direct upload" + auth_required: true + + # Search API + - id: api_search + method: GET + path: /api/search + description: "Search across all content" + auth_required: false + + - id: api_search_suggestions + method: GET + path: /api/search/suggestions + description: "Get search suggestions" + auth_required: false + + - id: api_search_index + method: POST + path: /api/search/index + description: "Index entity for search" + auth_required: true + +pages: + - id: page_auth_verify_email + name: EmailVerification + path: /auth/verify-email + description: "Email verification page" + data_needs: [] + components: [] + auth_required: false + + - id: page_settings_security + name: SecuritySettings + path: /settings/security + description: "Security settings page" + data_needs: + - api_id: api_auth_sessions + purpose: "Load active sessions" + on_load: true + components: + - component_session_list + - component_change_password + auth_required: true + +components: + - id: component_audio_player + name: AudioPlayer + description: "Full-featured audio player with controls" + props: + - name: song + type: object + required: false + description: "Current playing song" + - name: isPlaying + type: boolean + required: true + description: "Playback state" + - name: progress + type: number + required: true + description: "Current playback progress (0-1)" + - name: volume + type: number + required: true + description: "Volume level (0-1)" + - name: queue + type: object + required: false + description: "Current queue" + events: + - name: play + description: "Play or resume playback" + - name: pause + description: "Pause playback" + - name: seek + description: "Seek to position" + - name: volumeChange + description: "Change volume" + - name: next + description: "Play next song" + - name: previous + description: "Play previous song" + - name: shuffle + description: "Toggle shuffle" + - name: repeat + description: "Set repeat mode" + uses_apis: + - api_player_play + - api_player_pause + - api_player_next + - api_player_previous + - api_player_queue + + - id: component_upload_manager + name: UploadManager + description: "Handles chunked file uploads" + props: + - name: onUploadComplete + type: function + required: true + description: "Callback when upload completes" + - name: acceptedTypes + type: array + required: false + description: "Accepted file types" + - name: maxSize + type: number + required: false + description: "Max file size in bytes" + events: + - name: progress + description: "Upload progress update" + - name: error + description: "Upload error" + uses_apis: + - api_upload_init + - api_upload_chunk + - api_upload_complete + + - id: component_search_bar + name: SearchBar + description: "Search input with suggestions" + props: + - name: placeholder + type: string + required: false + description: "Input placeholder" + - name: autoFocus + type: boolean + required: false + description: "Auto focus input" + events: + - name: search + description: "Search query submitted" + - name: suggestionSelect + description: "Suggestion selected" + uses_apis: + - api_search_suggestions + + - id: component_search_results + name: SearchResults + description: "Display search results" + props: + - name: query + type: string + required: true + description: "Search query" + - name: filters + type: object + required: false + description: "Search filters" + events: + - name: playSong + description: "Play selected song" + - name: viewAlbum + description: "View album details" + - name: viewArtist + description: "View artist profile" + uses_apis: + - api_search \ No newline at end of file diff --git a/.workflow/versions/v005/requirements/expanded.yml b/.workflow/versions/v005/requirements/expanded.yml new file mode 100644 index 0000000..be54cef --- /dev/null +++ b/.workflow/versions/v005/requirements/expanded.yml @@ -0,0 +1,117 @@ +feature: "examine what is missing in current app and implement it" +expanded_at: "2025-12-20T00:00:00.000Z" +mode: full_auto + +analysis: + current_state: "Sonic Cloud is a Next.js music platform with foundational components and database schema in place. The app has a dark theme UI with header navigation, but most functionality is still in PENDING state in the manifest. The database schema is well-designed with models for User, Artist, Label, Genre, Album, Song, Playlist, and more. Some components and API routes exist but the core music player functionality is not connected." + app_type: "Music streaming and distribution platform" + core_purpose: "Provide a platform for musicians to upload, share, and distribute their music, and for users to discover, stream, and organize music" + +missing_critical: + - feature: "Audio Playback Engine" + reason: "Without a working audio player, the app cannot fulfill its core purpose as a music platform" + impact: "Users cannot listen to music, making the platform non-functional" + + - feature: "User Authentication System" + reason: "Currently no auth implementation despite having auth components and API routes defined" + impact: "Users cannot register, login, or have personalized experiences" + + - feature: "File Upload Infrastructure" + reason: "No actual file storage or streaming capability for audio files" + impact: "Artists cannot upload music, platform has no content to serve" + + - feature: "Music Discovery Features" + reason: "No recommendation algorithm, personalized content, or discovery beyond basic listings" + impact: "Poor user engagement and difficulty finding relevant music" + + - feature: "Search Functionality" + reason: "Search components exist but no backend implementation" + impact: "Users cannot find specific songs, artists, or albums" + +missing_important: + - feature: "User Library/Favorites" + reason: "No way for users to save or favorite songs they like" + impact: "Poor user retention and no personalized collection building" + + - feature: "Social Features" + reason: "No user profiles, following, or social interaction" + impact: "Limited community engagement and viral growth potential" + + - feature: "Mobile App or Responsive Design" + reason: "Desktop-only experience limits accessibility" + impact: "Missed mobile users who comprise majority of music streaming market" + + - feature: "Analytics for Artists" + reason: "Artists need data about their listeners and performance" + impact: "Reduced value proposition for artist users" + + - feature: "Playlist Collaboration" + reason: "No way to share or collaborate on playlists" + impact: "Limited social music discovery and curation" + +missing_nice_to_have: + - feature: "Podcast Support" + reason: "Expand platform beyond music to spoken word content" + impact: "Broader audience and increased engagement" + + - feature: "Live Streaming" + reason: "Allow artists to broadcast live performances" + impact: "Unique differentiator and community building tool" + + - feature: "AI-Powered Recommendations" + reason: "Better music discovery through machine learning" + impact: "Improved user experience and retention" + + - feature: "Integration with External Services" + reason: "Connect with Spotify, Apple Music, social media" + impact: "Easier onboarding and cross-platform presence" + + - feature: "Merchandise Store" + reason: "Allow artists to sell merchandise" + impact: "Additional revenue stream for artists and platform" + +technical_gaps: + - gap: "No WebSocket implementation for real-time features" + category: "performance" + priority: "high" + + - gap: "Missing CDN for audio file delivery" + category: "performance" + priority: "high" + + - gap: "No caching layer for API responses" + category: "performance" + priority: "medium" + + - gap: "No rate limiting on API endpoints" + category: "security" + priority: "high" + + - gap: "Missing input validation and sanitization" + category: "security" + priority: "high" + + - gap: "No content moderation system" + category: "security" + priority: "medium" + + - gap: "Limited accessibility features" + category: "accessibility" + priority: "medium" + + - gap: "No SEO optimization for dynamic content" + category: "seo" + priority: "medium" + + - gap: "No monitoring or error tracking" + category: "performance" + priority: "low" + + - gap: "No automated testing suite" + category: "quality" + priority: "medium" + +implementation_plan: + phase_1: "Implement core functionality - Audio player with controls, User authentication with NextAuth.js, File upload with Cloudinary/AWS S3, Basic search with full-text search, Database seeding with sample content" + phase_2: "Enhance user experience - User favorites/library, Improved music discovery with categories, Artist analytics dashboard, Playlist sharing features, Mobile-responsive design improvements" + phase_3: "Add advanced features - Social features (following, user profiles), Podcast support infrastructure, Basic recommendation engine, Performance optimizations (CDN, caching), Security hardening" \ No newline at end of file diff --git a/.workflow/versions/v005/requirements/final.yml b/.workflow/versions/v005/requirements/final.yml new file mode 100644 index 0000000..6e36a33 --- /dev/null +++ b/.workflow/versions/v005/requirements/final.yml @@ -0,0 +1,90 @@ +feature: "examine what is missing in current app and implement it" +mode: full_auto +finalized_at: 2025-12-20T22:05:00Z + +analysis: + current_state: "Next.js music streaming platform with complete data model but most features unimplemented" + app_type: "music streaming platform" + core_purpose: "Allow users to discover, stream, and manage music" + +missing_critical: + - feature: "Audio Playback Engine" + reason: "Core functionality - users cannot play music" + impact: "App is non-functional without audio playback" + - feature: "User Authentication" + reason: "No way to identify users or save preferences" + impact: "Cannot provide personalized experience" + - feature: "File Upload System" + reason: "No mechanism to add music to the platform" + impact: "Platform has no content to stream" + - feature: "Search Implementation" + reason: "UI exists but no backend search capability" + impact: "Users cannot discover music beyond browsing" + +missing_important: + - feature: "User Library/Favorites" + reason: "No way for users to save liked music" + impact: "Poor user retention and experience" + - feature: "Music Discovery" + reason: "No recommendations beyond basic listings" + impact: "Limited content exploration" + - feature: "Artist Analytics Dashboard" + reason: "Artists cannot track their performance" + impact: "No value proposition for content creators" + - feature: "Playlist Collaboration" + reason: "No social features or sharing" + impact: "Limited community engagement" + +missing_nice_to_have: + - feature: "Mobile App" + reason: "No native mobile experience" + impact: "Limited accessibility" + - feature: "AI Recommendations" + reason: "No personalized discovery engine" + impact: "Generic user experience" + - feature: "Live Streaming" + reason: "No real-time performance features" + impact: "Limited artist engagement" + - feature: "Social Features" + reason: "No user profiles or following" + impact: "No community building" + +technical_gaps: + - gap: "No WebSocket implementation" + category: "real-time" + priority: "high" + - gap: "No CDN for audio delivery" + category: "performance" + priority: "high" + - gap: "No API rate limiting" + category: "security" + priority: "medium" + - gap: "No caching layer" + category: "performance" + priority: "medium" + - gap: "Limited accessibility features" + category: "accessibility" + priority: "low" + +acceptance_criteria: + - criterion: "Users can play audio tracks" + verification: "Click play on any song and verify audio plays with controls" + - criterion: "Users can register and login" + verification: "Complete registration flow, verify login works, session persists" + - criterion: "Users can upload music files" + verification: "Upload mp3/other format, verify file stored and appears in library" + - criterion: "Search returns relevant results" + verification: "Search for songs/artists/albums, verify accurate results" + - criterion: "Player controls work properly" + verification: "Play, pause, skip, volume, seek all function correctly" + - criterion: "Queue system works" + verification: "Add songs to queue, verify they play in order" + - criterion: "User can save favorites" + verification: "Like songs, verify they appear in favorites list" + - criterion: "Responsive design works" + verification: "Test on mobile/tablet, verify layout adapts properly" + +implementation_plan: + phase_1: "Audio player, authentication, file upload, search implementation" + phase_2: "User library, favorites, basic discovery, artist dashboard" + phase_3: "Social features, advanced recommendations, performance optimizations" \ No newline at end of file diff --git a/.workflow/versions/v005/session.yml b/.workflow/versions/v005/session.yml new file mode 100644 index 0000000..cd1393b --- /dev/null +++ b/.workflow/versions/v005/session.yml @@ -0,0 +1,30 @@ +version: v005 +feature: examine what is missing in current app and implement it +session_id: workflow_20251220_220210 +parent_version: null +status: completed +started_at: '2025-12-20T22:02:10.309550' +completed_at: '2025-12-20T22:28:45.494863' +current_phase: COMPLETING +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-20T22:11:01.686710' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-20T22:28:32.697728' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-20T22:28:45.494868' diff --git a/.workflow/versions/v005/session.yml.bak b/.workflow/versions/v005/session.yml.bak new file mode 100644 index 0000000..8942e92 --- /dev/null +++ b/.workflow/versions/v005/session.yml.bak @@ -0,0 +1,30 @@ +version: v005 +feature: examine what is missing in current app and implement it +session_id: workflow_20251220_220210 +parent_version: null +status: pending +started_at: '2025-12-20T22:02:10.309550' +completed_at: null +current_phase: COMPLETING +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-20T22:11:01.686710' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-20T22:28:32.697728' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-20T22:28:32.740115' diff --git a/.workflow/versions/v005/snapshot_after/manifest.json b/.workflow/versions/v005/snapshot_after/manifest.json new file mode 100644 index 0000000..d291b70 --- /dev/null +++ b/.workflow/versions/v005/snapshot_after/manifest.json @@ -0,0 +1,1360 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + }, + { + "action": "DESIGN_DOCUMENT_UPDATED", + "timestamp": "2025-12-20T22:10:00", + "details": "Added Phase 1 critical features: Enhanced authentication, advanced audio playback, file upload system, search implementation" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_share", + "name": "Share", + "table_name": "shares", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_refresh_token", + "name": "RefreshToken", + "table_name": "refresh_tokens", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_session", + "name": "Session", + "table_name": "sessions", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_play_history", + "name": "PlayHistory", + "table_name": "play_histories", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_queue", + "name": "Queue", + "table_name": "queues", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_player_state", + "name": "PlayerState", + "table_name": "player_states", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_audio_file", + "name": "AudioFile", + "table_name": "audio_files", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_upload_session", + "name": "UploadSession", + "table_name": "upload_sessions", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_search_index", + "name": "SearchIndex", + "table_name": "search_indices", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_search_query", + "name": "SearchQuery", + "table_name": "search_queries", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_search_suggestion", + "name": "SearchSuggestion", + "table_name": "search_suggestions", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + }, + { + "id": "api_create_song_share", + "name": "Create Song Share", + "path": "/api/share/song/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/song/[id]/route.ts" + }, + { + "id": "api_create_playlist_share", + "name": "Create Playlist Share", + "path": "/api/share/playlist/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/playlist/[id]/route.ts" + }, + { + "id": "api_create_album_share", + "name": "Create Album Share", + "path": "/api/share/album/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/album/[id]/route.ts" + }, + { + "id": "api_resolve_share", + "name": "Resolve Share", + "path": "/api/share/:token", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/share/[token]/route.ts" + }, + { + "id": "api_track_share_click", + "name": "Track Share Click", + "path": "/api/share/:token/click", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/[token]/click/route.ts" + }, + { + "id": "api_refresh_token", + "name": "Refresh Token", + "path": "/api/auth/refresh", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/refresh/route.ts" + }, + { + "id": "api_logout", + "name": "Logout", + "path": "/api/auth/logout", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/logout/route.ts" + }, + { + "id": "api_get_sessions", + "name": "Get Sessions", + "path": "/api/auth/sessions", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/auth/sessions/route.ts" + }, + { + "id": "api_revoke_session", + "name": "Revoke Session", + "path": "/api/auth/sessions/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/auth/sessions/[id]/route.ts" + }, + { + "id": "api_verify_email", + "name": "Verify Email", + "path": "/api/auth/verify-email", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/verify-email/route.ts" + }, + { + "id": "api_confirm_email", + "name": "Confirm Email", + "path": "/api/auth/confirm-email", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/confirm-email/route.ts" + }, + { + "id": "api_change_password", + "name": "Change Password", + "path": "/api/auth/change-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/change-password/route.ts" + }, + { + "id": "api_get_queue", + "name": "Get Queue", + "path": "/api/player/queue", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/player/queue/route.ts" + }, + { + "id": "api_update_queue", + "name": "Update Queue", + "path": "/api/player/queue", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/player/queue/route.ts" + }, + { + "id": "api_add_to_queue", + "name": "Add to Queue", + "path": "/api/player/queue/add", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/queue/add/route.ts" + }, + { + "id": "api_remove_from_queue", + "name": "Remove from Queue", + "path": "/api/player/queue/remove", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/player/queue/remove/route.ts" + }, + { + "id": "api_reorder_queue", + "name": "Reorder Queue", + "path": "/api/player/queue/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/player/queue/reorder/route.ts" + }, + { + "id": "api_clear_queue", + "name": "Clear Queue", + "path": "/api/player/queue/clear", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/player/queue/clear/route.ts" + }, + { + "id": "api_get_player_state", + "name": "Get Player State", + "path": "/api/player/state", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/player/state/route.ts" + }, + { + "id": "api_update_player_state", + "name": "Update Player State", + "path": "/api/player/state", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/player/state/route.ts" + }, + { + "id": "api_player_play", + "name": "Player Play", + "path": "/api/player/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/play/route.ts" + }, + { + "id": "api_player_pause", + "name": "Player Pause", + "path": "/api/player/pause", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/pause/route.ts" + }, + { + "id": "api_player_next", + "name": "Player Next", + "path": "/api/player/next", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/next/route.ts" + }, + { + "id": "api_player_previous", + "name": "Player Previous", + "path": "/api/player/previous", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/previous/route.ts" + }, + { + "id": "api_player_seek", + "name": "Player Seek", + "path": "/api/player/seek", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/seek/route.ts" + }, + { + "id": "api_player_shuffle", + "name": "Player Shuffle", + "path": "/api/player/shuffle", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/shuffle/route.ts" + }, + { + "id": "api_player_repeat", + "name": "Player Repeat", + "path": "/api/player/repeat", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/repeat/route.ts" + }, + { + "id": "api_get_play_history", + "name": "Get Play History", + "path": "/api/player/history", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/player/history/route.ts" + }, + { + "id": "api_record_play", + "name": "Record Play", + "path": "/api/player/history", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/player/history/route.ts" + }, + { + "id": "api_analyze_audio", + "name": "Analyze Audio", + "path": "/api/player/analyze/:songId", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/player/analyze/[songId]/route.ts" + }, + { + "id": "api_initiate_upload", + "name": "Initiate Upload", + "path": "/api/upload/initiate", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/upload/initiate/route.ts" + }, + { + "id": "api_upload_chunk", + "name": "Upload Chunk", + "path": "/api/upload/chunk/:sessionId", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/upload/chunk/[sessionId]/route.ts" + }, + { + "id": "api_complete_upload", + "name": "Complete Upload", + "path": "/api/upload/complete/:sessionId", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/upload/complete/[sessionId]/route.ts" + }, + { + "id": "api_upload_status", + "name": "Upload Status", + "path": "/api/upload/status/:sessionId", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/upload/status/[sessionId]/route.ts" + }, + { + "id": "api_abort_upload", + "name": "Abort Upload", + "path": "/api/upload/abort/:sessionId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/upload/abort/[sessionId]/route.ts" + }, + { + "id": "api_process_audio", + "name": "Process Audio", + "path": "/api/upload/process/:fileId", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/upload/process/[fileId]/route.ts" + }, + { + "id": "api_audio_preview", + "name": "Audio Preview", + "path": "/api/upload/preview/:fileId", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/upload/preview/[fileId]/route.ts" + }, + { + "id": "api_storage_usage", + "name": "Storage Usage", + "path": "/api/upload/storage/usage", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/upload/storage/usage/route.ts" + }, + { + "id": "api_list_files", + "name": "List Files", + "path": "/api/upload/files", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/upload/files/route.ts" + }, + { + "id": "api_delete_file", + "name": "Delete File", + "path": "/api/upload/files/:fileId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/upload/files/[fileId]/route.ts" + }, + { + "id": "api_search_suggestions", + "name": "Search Suggestions", + "path": "/api/search/suggestions", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/suggestions/route.ts" + }, + { + "id": "api_trending_searches", + "name": "Trending Searches", + "path": "/api/search/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/trending/route.ts" + }, + { + "id": "api_recent_searches", + "name": "Recent Searches", + "path": "/api/search/recent/:userId", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/recent/[userId]/route.ts" + }, + { + "id": "api_save_search", + "name": "Save Search", + "path": "/api/search/save/:userId", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/search/save/[userId]/route.ts" + }, + { + "id": "api_index_search", + "name": "Index Search", + "path": "/api/search/index", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/search/index/route.ts" + }, + { + "id": "api_update_search_index", + "name": "Update Search Index", + "path": "/api/search/index/:entityType/:entityId", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/search/index/[entityType]/[entityId]/route.ts" + }, + { + "id": "api_remove_search_index", + "name": "Remove Search Index", + "path": "/api/search/index/:entityType/:entityId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/search/index/[entityType]/[entityId]/route.ts" + }, + { + "id": "api_search_analytics", + "name": "Search Analytics", + "path": "/api/search/analytics", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/analytics/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + }, + { + "id": "page_share", + "name": "Share", + "path": "/s/:token", + "status": "PENDING", + "file_path": "app/s/[token]/page.tsx" + }, + { + "id": "page_verify_email", + "name": "Email Verification", + "path": "/auth/verify-email", + "status": "PENDING", + "file_path": "app/auth/verify-email/page.tsx" + }, + { + "id": "page_security_settings", + "name": "Security Settings", + "path": "/settings/security", + "status": "PENDING", + "file_path": "app/settings/security/page.tsx" + }, + { + "id": "page_connected_accounts", + "name": "Connected Accounts", + "path": "/settings/connected-accounts", + "status": "PENDING", + "file_path": "app/settings/connected-accounts/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + }, + { + "id": "component_header", + "name": "Header", + "status": "IMPLEMENTED", + "file_path": "components/Header.tsx" + }, + { + "id": "component_nav_link", + "name": "NavLink", + "status": "IMPLEMENTED", + "file_path": "components/NavLink.tsx" + }, + { + "id": "component_user_menu", + "name": "UserMenu", + "status": "IMPLEMENTED", + "file_path": "components/UserMenu.tsx" + }, + { + "id": "component_share_button", + "name": "ShareButton", + "status": "PENDING", + "file_path": "components/ShareButton.tsx" + }, + { + "id": "component_share_modal", + "name": "ShareModal", + "status": "PENDING", + "file_path": "components/ShareModal.tsx" + }, + { + "id": "component_social_share_buttons", + "name": "SocialShareButtons", + "status": "PENDING", + "file_path": "components/SocialShareButtons.tsx" + }, + { + "id": "component_share_content_display", + "name": "SharedContentDisplay", + "status": "PENDING", + "file_path": "components/SharedContentDisplay.tsx" + }, + { + "id": "component_advanced_audio_player", + "name": "AdvancedAudioPlayer", + "status": "PENDING", + "file_path": "components/AdvancedAudioPlayer.tsx" + }, + { + "id": "component_queue_manager", + "name": "QueueManager", + "status": "PENDING", + "file_path": "components/QueueManager.tsx" + }, + { + "id": "component_audio_visualizer", + "name": "AudioVisualizer", + "status": "PENDING", + "file_path": "components/AudioVisualizer.tsx" + }, + { + "id": "component_mini_player", + "name": "MiniPlayer", + "status": "PENDING", + "file_path": "components/MiniPlayer.tsx" + }, + { + "id": "component_play_history", + "name": "PlayHistory", + "status": "PENDING", + "file_path": "components/PlayHistory.tsx" + }, + { + "id": "component_advanced_upload_form", + "name": "AdvancedUploadForm", + "status": "PENDING", + "file_path": "components/AdvancedUploadForm.tsx" + }, + { + "id": "component_upload_progress", + "name": "UploadProgress", + "status": "PENDING", + "file_path": "components/UploadProgress.tsx" + }, + { + "id": "component_file_uploader", + "name": "FileUploader", + "status": "PENDING", + "file_path": "components/FileUploader.tsx" + }, + { + "id": "component_audio_preview", + "name": "AudioPreview", + "status": "PENDING", + "file_path": "components/AudioPreview.tsx" + }, + { + "id": "component_advanced_search_bar", + "name": "AdvancedSearchBar", + "status": "PENDING", + "file_path": "components/AdvancedSearchBar.tsx" + }, + { + "id": "component_search_filters", + "name": "SearchFilters", + "status": "PENDING", + "file_path": "components/SearchFilters.tsx" + }, + { + "id": "component_search_analytics", + "name": "SearchAnalytics", + "status": "PENDING", + "file_path": "components/SearchAnalytics.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": [ + "page_login", + "page_register", + "page_forgot_password" + ], + "component_song_card": [ + "page_home", + "page_artist_profile", + "page_search", + "page_genre_browse", + "page_album_detail", + "page_playlist_detail" + ], + "component_genre_badge": [ + "page_home" + ], + "component_section_header": [ + "page_home" + ], + "component_artist_header": [ + "page_artist_profile" + ], + "component_album_card": [ + "page_artist_profile", + "page_search" + ], + "component_social_links": [ + "page_artist_profile" + ], + "component_album_header": [ + "page_album_detail" + ], + "component_track_list": [ + "page_album_detail", + "page_playlist_detail" + ], + "component_upload_form": [ + "page_upload" + ], + "component_waveform_display": [ + "page_upload" + ], + "component_playlist_card": [ + "page_playlists" + ], + "component_create_playlist_modal": [ + "page_playlists" + ], + "component_playlist_header": [ + "page_playlist_detail" + ], + "component_profile_form": [ + "page_profile" + ], + "component_avatar_upload": [ + "page_profile" + ], + "component_search_bar": [ + "page_search" + ], + "component_search_results": [ + "page_search" + ], + "component_artist_card": [ + "page_search" + ], + "component_genre_header": [ + "page_genre_browse" + ] + }, + "api_to_component": { + "api_login": [ + "component_auth_form" + ], + "api_register": [ + "component_auth_form" + ], + "api_forgot_password": [ + "component_auth_form" + ], + "api_upload_song": [ + "component_upload_form" + ], + "api_increment_play_count": [ + "component_audio_player" + ], + "api_search": [ + "component_search_bar" + ], + "api_create_playlist": [ + "component_create_playlist_modal" + ], + "api_update_current_user": [ + "component_profile_form" + ] + }, + "table_to_api": { + "model_user": [ + "api_register", + "api_login", + "api_forgot_password", + "api_reset_password", + "api_get_current_user", + "api_update_current_user" + ], + "model_artist": [ + "api_create_artist_profile", + "api_get_artist", + "api_update_artist", + "api_get_artist_songs", + "api_get_artist_albums" + ], + "model_song": [ + "api_upload_song", + "api_get_song", + "api_update_song", + "api_delete_song", + "api_increment_play_count", + "api_get_artist_songs", + "api_get_trending_songs", + "api_get_new_releases", + "api_search" + ], + "model_album": [ + "api_create_album", + "api_get_album", + "api_update_album", + "api_delete_album", + "api_get_artist_albums", + "api_search" + ], + "model_playlist": [ + "api_create_playlist", + "api_get_user_playlists", + "api_get_playlist", + "api_update_playlist", + "api_delete_playlist" + ], + "model_playlist_song": [ + "api_add_song_to_playlist", + "api_remove_song_from_playlist", + "api_reorder_playlist_songs" + ], + "model_genre": [ + "api_get_genres", + "api_get_songs_by_genre" + ], + "model_label": [ + "api_create_label_profile", + "api_get_label_artists" + ] + } + } +} \ No newline at end of file diff --git a/.workflow/versions/v005/snapshot_before/manifest.json b/.workflow/versions/v005/snapshot_before/manifest.json new file mode 100644 index 0000000..0754607 --- /dev/null +++ b/.workflow/versions/v005/snapshot_before/manifest.json @@ -0,0 +1,737 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_share", + "name": "Share", + "table_name": "shares", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + }, + { + "id": "api_create_song_share", + "name": "Create Song Share", + "path": "/api/share/song/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/song/[id]/route.ts" + }, + { + "id": "api_create_playlist_share", + "name": "Create Playlist Share", + "path": "/api/share/playlist/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/playlist/[id]/route.ts" + }, + { + "id": "api_create_album_share", + "name": "Create Album Share", + "path": "/api/share/album/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/album/[id]/route.ts" + }, + { + "id": "api_resolve_share", + "name": "Resolve Share", + "path": "/api/share/:token", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/share/[token]/route.ts" + }, + { + "id": "api_track_share_click", + "name": "Track Share Click", + "path": "/api/share/:token/click", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/[token]/click/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + }, + { + "id": "page_share", + "name": "Share", + "path": "/s/:token", + "status": "PENDING", + "file_path": "app/s/[token]/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + }, + { + "id": "component_header", + "name": "Header", + "status": "IMPLEMENTED", + "file_path": "components/Header.tsx" + }, + { + "id": "component_nav_link", + "name": "NavLink", + "status": "IMPLEMENTED", + "file_path": "components/NavLink.tsx" + }, + { + "id": "component_user_menu", + "name": "UserMenu", + "status": "IMPLEMENTED", + "file_path": "components/UserMenu.tsx" + }, + { + "id": "component_share_button", + "name": "ShareButton", + "status": "PENDING", + "file_path": "components/ShareButton.tsx" + }, + { + "id": "component_share_modal", + "name": "ShareModal", + "status": "PENDING", + "file_path": "components/ShareModal.tsx" + }, + { + "id": "component_social_share_buttons", + "name": "SocialShareButtons", + "status": "PENDING", + "file_path": "components/SocialShareButtons.tsx" + }, + { + "id": "component_share_content_display", + "name": "SharedContentDisplay", + "status": "PENDING", + "file_path": "components/SharedContentDisplay.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": ["page_login", "page_register", "page_forgot_password"], + "component_song_card": ["page_home", "page_artist_profile", "page_search", "page_genre_browse", "page_album_detail", "page_playlist_detail"], + "component_genre_badge": ["page_home"], + "component_section_header": ["page_home"], + "component_artist_header": ["page_artist_profile"], + "component_album_card": ["page_artist_profile", "page_search"], + "component_social_links": ["page_artist_profile"], + "component_album_header": ["page_album_detail"], + "component_track_list": ["page_album_detail", "page_playlist_detail"], + "component_upload_form": ["page_upload"], + "component_waveform_display": ["page_upload"], + "component_playlist_card": ["page_playlists"], + "component_create_playlist_modal": ["page_playlists"], + "component_playlist_header": ["page_playlist_detail"], + "component_profile_form": ["page_profile"], + "component_avatar_upload": ["page_profile"], + "component_search_bar": ["page_search"], + "component_search_results": ["page_search"], + "component_artist_card": ["page_search"], + "component_genre_header": ["page_genre_browse"] + }, + "api_to_component": { + "api_login": ["component_auth_form"], + "api_register": ["component_auth_form"], + "api_forgot_password": ["component_auth_form"], + "api_upload_song": ["component_upload_form"], + "api_increment_play_count": ["component_audio_player"], + "api_search": ["component_search_bar"], + "api_create_playlist": ["component_create_playlist_modal"], + "api_update_current_user": ["component_profile_form"] + }, + "table_to_api": { + "model_user": ["api_register", "api_login", "api_forgot_password", "api_reset_password", "api_get_current_user", "api_update_current_user"], + "model_artist": ["api_create_artist_profile", "api_get_artist", "api_update_artist", "api_get_artist_songs", "api_get_artist_albums"], + "model_song": ["api_upload_song", "api_get_song", "api_update_song", "api_delete_song", "api_increment_play_count", "api_get_artist_songs", "api_get_trending_songs", "api_get_new_releases", "api_search"], + "model_album": ["api_create_album", "api_get_album", "api_update_album", "api_delete_album", "api_get_artist_albums", "api_search"], + "model_playlist": ["api_create_playlist", "api_get_user_playlists", "api_get_playlist", "api_update_playlist", "api_delete_playlist"], + "model_playlist_song": ["api_add_song_to_playlist", "api_remove_song_from_playlist", "api_reorder_playlist_songs"], + "model_genre": ["api_get_genres", "api_get_songs_by_genre"], + "model_label": ["api_create_label_profile", "api_get_label_artists"] + } + } +} diff --git a/app/api/auth/confirm-email/route.ts b/app/api/auth/confirm-email/route.ts new file mode 100644 index 0000000..0c2b532 --- /dev/null +++ b/app/api/auth/confirm-email/route.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { token } = body + + if (!token) { + return NextResponse.json( + { error: 'Verification token is required' }, + { status: 400 } + ) + } + + // Find user with this verification token + const user = await prisma.user.findFirst({ + where: { + emailToken: token, + resetExpires: { + gt: new Date(), + }, + }, + select: { + id: true, + email: true, + emailVerified: true, + displayName: true, + }, + }) + + if (!user) { + return NextResponse.json( + { error: 'Invalid or expired verification token' }, + { status: 400 } + ) + } + + if (user.emailVerified) { + return NextResponse.json( + { error: 'Email is already verified' }, + { status: 400 } + ) + } + + // Verify the email + await prisma.user.update({ + where: { id: user.id }, + data: { + emailVerified: true, + emailToken: null, + resetExpires: null, + }, + }) + + return NextResponse.json( + { + message: 'Email verified successfully', + user: { + id: user.id, + email: user.email, + displayName: user.displayName, + emailVerified: true, + }, + }, + { status: 200 } + ) + } catch (error) { + console.error('Confirm email error:', error) + return NextResponse.json( + { error: 'Failed to verify email' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..f198333 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { getCurrentUser, revokeAllSessions, revokeAllRefreshTokens } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + // Get current user + const user = await getCurrentUser() + if (!user) { + return NextResponse.json( + { error: 'Not authenticated' }, + { status: 401 } + ) + } + + // Revoke all sessions and refresh tokens for this user + await Promise.all([ + revokeAllSessions(user.id), + revokeAllRefreshTokens(user.id), + ]) + + // Clear the auth cookie + const response = NextResponse.json( + { message: 'Logged out successfully' }, + { status: 200 } + ) + + response.cookies.set('auth-token', '', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 0, + path: '/', + }) + + return response + } catch (error) { + console.error('Logout error:', error) + return NextResponse.json( + { error: 'Failed to logout' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts new file mode 100644 index 0000000..6651d2e --- /dev/null +++ b/app/api/auth/refresh/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { generateToken, validateRefreshToken, createRefreshToken } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { refreshToken } = body + + if (!refreshToken) { + return NextResponse.json( + { error: 'Refresh token is required' }, + { status: 400 } + ) + } + + // Validate the refresh token + const user = await validateRefreshToken(refreshToken) + if (!user) { + return NextResponse.json( + { error: 'Invalid or expired refresh token' }, + { status: 401 } + ) + } + + // Generate new access token + const accessToken = generateToken({ + userId: user.id, + email: user.email, + role: user.role, + }) + + // Generate new refresh token (token rotation) + const newRefreshToken = await createRefreshToken(user.id) + + // Set the new access token in cookie + const response = NextResponse.json( + { + accessToken, + refreshToken: newRefreshToken, + user: { + id: user.id, + email: user.email, + username: user.username, + role: user.role, + }, + }, + { status: 200 } + ) + + response.cookies.set('auth-token', accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 15, // 15 minutes + path: '/', + }) + + return response + } catch (error) { + console.error('Token refresh error:', error) + return NextResponse.json( + { error: 'Failed to refresh token' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/auth/sessions/[id]/route.ts b/app/api/auth/sessions/[id]/route.ts new file mode 100644 index 0000000..72fe359 --- /dev/null +++ b/app/api/auth/sessions/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth, revokeSession } from '@/lib/auth' + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id: sessionId } = await params + + // First check if the session belongs to the current user + const session = await prisma.session.findUnique({ + where: { id: sessionId }, + select: { userId: true, token: true }, + }) + + if (!session) { + return NextResponse.json( + { error: 'Session not found' }, + { status: 404 } + ) + } + + if (session.userId !== user.id) { + return NextResponse.json( + { error: 'Forbidden' }, + { status: 403 } + ) + } + + // Revoke the session + await revokeSession(session.token) + + return NextResponse.json( + { message: 'Session revoked successfully' }, + { status: 200 } + ) + } catch (error) { + console.error('Revoke session error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to revoke session' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/auth/sessions/route.ts b/app/api/auth/sessions/route.ts new file mode 100644 index 0000000..f99f4b2 --- /dev/null +++ b/app/api/auth/sessions/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth, getUserSessions } from '@/lib/auth' + +export async function GET(request: NextRequest) { + try { + const user = await requireAuth() + + const sessions = await getUserSessions(user.id) + + // Parse deviceInfo JSON for each session + const formattedSessions = sessions.map(session => ({ + ...session, + deviceInfo: session.deviceInfo ? JSON.parse(session.deviceInfo) : undefined, + })) + + return NextResponse.json({ sessions: formattedSessions }, { status: 200 }) + } catch (error) { + console.error('Get sessions error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to fetch sessions' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/auth/verify-email/route.ts b/app/api/auth/verify-email/route.ts new file mode 100644 index 0000000..36c42d6 --- /dev/null +++ b/app/api/auth/verify-email/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { generateEmailToken } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { email } = body + + if (!email) { + return NextResponse.json( + { error: 'Email is required' }, + { status: 400 } + ) + } + + // Check if user exists + const user = await prisma.user.findUnique({ + where: { email }, + select: { id: true, emailVerified: true, displayName: true }, + }) + + if (!user) { + // Don't reveal that email doesn't exist + return NextResponse.json( + { message: 'If the email exists, a verification link has been sent' }, + { status: 200 } + ) + } + + if (user.emailVerified) { + return NextResponse.json( + { error: 'Email is already verified' }, + { status: 400 } + ) + } + + // Generate verification token + const emailToken = generateEmailToken() + const expiresAt = new Date() + expiresAt.setHours(expiresAt.getHours() + 24) // 24 hours + + // Update user with verification token + await prisma.user.update({ + where: { id: user.id }, + data: { + emailToken, + resetExpires: expiresAt, // Using resetExpires field for email token expiry + }, + }) + + // TODO: Send actual email with verification link + // For now, just log the token (in production, use a proper email service) + console.log(`Email verification for ${email}: ${emailToken}`) + + // The verification URL would be: + // `${process.env.NEXT_PUBLIC_APP_URL}/auth/confirm-email?token=${emailToken}` + + return NextResponse.json( + { message: 'If the email exists, a verification link has been sent' }, + { status: 200 } + ) + } catch (error) { + console.error('Send verification email error:', error) + return NextResponse.json( + { error: 'Failed to send verification email' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/player/history/route.ts b/app/api/player/history/route.ts new file mode 100644 index 0000000..d9002ee --- /dev/null +++ b/app/api/player/history/route.ts @@ -0,0 +1,93 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getPlayHistory, updatePlayHistoryItem } from '@/lib/player' + +export async function GET(request: NextRequest) { + try { + const user = await requireAuth() + const { searchParams } = new URL(request.url) + + const limit = parseInt(searchParams.get('limit') || '50') + const offset = parseInt(searchParams.get('offset') || '0') + + // Validate pagination parameters + if (limit < 1 || limit > 100) { + return NextResponse.json( + { error: 'Limit must be between 1 and 100' }, + { status: 400 } + ) + } + + if (offset < 0) { + return NextResponse.json( + { error: 'Offset must be non-negative' }, + { status: 400 } + ) + } + + const history = await getPlayHistory(user.id, limit, offset) + + return NextResponse.json( + { history, limit, offset }, + { status: 200 } + ) + } catch (error) { + console.error('Get play history error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to fetch play history' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + const body = await request.json() + const { songId, playedDuration, completed } = body + + if (!songId) { + return NextResponse.json( + { error: 'Song ID is required' }, + { status: 400 } + ) + } + + if (typeof playedDuration !== 'number' || playedDuration < 0) { + return NextResponse.json( + { error: 'Played duration must be a non-negative number' }, + { status: 400 } + ) + } + + await updatePlayHistoryItem( + user.id, + songId, + playedDuration, + completed || false + ) + + return NextResponse.json( + { message: 'Play history updated' }, + { status: 200 } + ) + } catch (error) { + console.error('Update play history error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to update play history' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/player/next/route.ts b/app/api/player/next/route.ts new file mode 100644 index 0000000..a557116 --- /dev/null +++ b/app/api/player/next/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getNextInQueue, playSong } from '@/lib/player' + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + + const nextSongId = await getNextInQueue(user.id) + if (!nextSongId) { + return NextResponse.json( + { error: 'No next song in queue' }, + { status: 404 } + ) + } + + await playSong(user.id, nextSongId, 'queue') + + return NextResponse.json( + { message: 'Playing next song', songId: nextSongId }, + { status: 200 } + ) + } catch (error) { + console.error('Next song error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to play next song' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/player/pause/route.ts b/app/api/player/pause/route.ts new file mode 100644 index 0000000..5987d6d --- /dev/null +++ b/app/api/player/pause/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + + // In a real implementation, you might want to: + // 1. Store the current playback state in Redis or database + // 2. Update the currently playing song's play duration + // 3. Handle pause for streaming services + + // For now, just return success + return NextResponse.json( + { message: 'Playback paused' }, + { status: 200 } + ) + } catch (error) { + console.error('Pause error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to pause playback' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/player/play/route.ts b/app/api/player/play/route.ts new file mode 100644 index 0000000..b7d38cb --- /dev/null +++ b/app/api/player/play/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' +import { playSong, getNextInQueue } from '@/lib/player' + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + const body = await request.json() + const { songId } = body + + // If a specific songId is provided, play that song + if (songId) { + // Verify the song exists and is public + const song = await prisma.song.findUnique({ + where: { id: songId }, + select: { id: true, isPublic: true, artistId: true }, + }) + + if (!song) { + return NextResponse.json( + { error: 'Song not found' }, + { status: 404 } + ) + } + + if (!song.isPublic && song.artistId !== user.id) { + return NextResponse.json( + { error: 'Cannot play this song' }, + { status: 403 } + ) + } + + await playSong(user.id, songId, 'manual') + return NextResponse.json( + { message: 'Playing song', songId }, + { status: 200 } + ) + } + + // If no songId, play next in queue + const nextSongId = await getNextInQueue(user.id) + if (!nextSongId) { + return NextResponse.json( + { error: 'No songs in queue' }, + { status: 404 } + ) + } + + await playSong(user.id, nextSongId, 'queue') + return NextResponse.json( + { message: 'Playing next song in queue', songId: nextSongId }, + { status: 200 } + ) + } catch (error) { + console.error('Play error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to play song' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/player/previous/route.ts b/app/api/player/previous/route.ts new file mode 100644 index 0000000..fc91044 --- /dev/null +++ b/app/api/player/previous/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getPreviousInQueue, playSong } from '@/lib/player' + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + + const previousSongId = await getPreviousInQueue(user.id) + if (!previousSongId) { + return NextResponse.json( + { error: 'No previous song in queue' }, + { status: 404 } + ) + } + + await playSong(user.id, previousSongId, 'queue') + + return NextResponse.json( + { message: 'Playing previous song', songId: previousSongId }, + { status: 200 } + ) + } catch (error) { + console.error('Previous song error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to play previous song' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/player/queue/route.ts b/app/api/player/queue/route.ts new file mode 100644 index 0000000..ff4e002 --- /dev/null +++ b/app/api/player/queue/route.ts @@ -0,0 +1,161 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getUserQueue, addToQueue, updateUserQueue } from '@/lib/player' +import { prisma } from '@/lib/prisma' + +export async function GET(request: NextRequest) { + try { + const user = await requireAuth() + const queue = await getUserQueue(user.id) + + // Get song details for the queue + const songIds = queue.songIds as string[] || [] + if (songIds.length === 0) { + return NextResponse.json( + { + ...queue, + songIds: [], + songs: [], + }, + { status: 200 } + ) + } + + const songs = await prisma.song.findMany({ + where: { + id: { in: songIds }, + isPublic: true, + }, + select: { + id: true, + title: true, + slug: true, + duration: true, + coverUrl: true, + artist: { + select: { + id: true, + name: true, + slug: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + }, + }, + }, + }) + + // Order songs according to queue order + const orderedSongs = songIds.map(id => songs.find(song => song.id === id)).filter(Boolean) + + return NextResponse.json( + { + ...queue, + songIds, + songs: orderedSongs, + }, + { status: 200 } + ) + } catch (error) { + console.error('Get queue error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to fetch queue' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + const body = await request.json() + const { songIds, currentIndex, isShuffled, repeatMode } = body + + // Validate song IDs + if (songIds && Array.isArray(songIds)) { + const songs = await prisma.song.findMany({ + where: { + id: { in: songIds }, + OR: [ + { isPublic: true }, + { artistId: user.id }, + ], + }, + select: { id: true }, + }) + + const validSongIds = songs.map(song => song.id) + const invalidSongIds = songIds.filter(id => !validSongIds.includes(id)) + + if (invalidSongIds.length > 0) { + return NextResponse.json( + { error: 'Invalid song IDs', invalidSongs: invalidSongIds }, + { status: 400 } + ) + } + + const queue = await addToQueue(user.id, songIds) + return NextResponse.json(queue, { status: 200 }) + } + + // Update queue properties + const currentQueue = await getUserQueue(user.id) + const updatedQueue = await updateUserQueue( + user.id, + currentQueue.songIds as string[] || [], + currentIndex, + isShuffled, + repeatMode + ) + + return NextResponse.json(updatedQueue, { status: 200 }) + } catch (error) { + console.error('Update queue error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to update queue' }, + { status: 500 } + ) + } +} + +export async function DELETE(request: NextRequest) { + try { + const user = await requireAuth() + + // Clear the queue + const queue = await updateUserQueue(user.id, [], 0, false, 'none') + + return NextResponse.json( + { message: 'Queue cleared', queue }, + { status: 200 } + ) + } catch (error) { + console.error('Clear queue error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to clear queue' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/search/index/route.ts b/app/api/search/index/route.ts new file mode 100644 index 0000000..1132e05 --- /dev/null +++ b/app/api/search/index/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { indexEntity, reindexAll } from '@/lib/search' + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + const body = await request.json() + const { action, entityType, entityId, title, content, metadata } = body + + // Only allow admins or content owners to index entities + if (user.role !== 'admin') { + return NextResponse.json( + { error: 'Admin access required' }, + { status: 403 } + ) + } + + if (action === 'reindex-all') { + // Reindex all entities - admin only operation + await reindexAll() + return NextResponse.json( + { message: 'Search index rebuilt successfully' }, + { status: 200 } + ) + } + + if (action === 'index-entity') { + // Index a single entity + if (!entityType || !entityId || !title) { + return NextResponse.json( + { error: 'entityType, entityId, and title are required' }, + { status: 400 } + ) + } + + const validTypes = ['song', 'album', 'artist', 'playlist'] + if (!validTypes.includes(entityType)) { + return NextResponse.json( + { error: 'Invalid entityType. Must be one of: song, album, artist, playlist' }, + { status: 400 } + ) + } + + const indexedEntity = await indexEntity( + entityType, + entityId, + title, + content, + metadata + ) + + return NextResponse.json( + { + message: 'Entity indexed successfully', + entity: indexedEntity, + }, + { status: 201 } + ) + } + + return NextResponse.json( + { error: 'Invalid action. Must be "reindex-all" or "index-entity"' }, + { status: 400 } + ) + } catch (error) { + console.error('Search index error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to update search index' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 4f81d70..e3cfed2 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -1,129 +1,76 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/prisma' +import { searchEntities } from '@/lib/search' export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) const query = searchParams.get('q') + const type = searchParams.get('type') const limit = parseInt(searchParams.get('limit') || '20') + const offset = parseInt(searchParams.get('offset') || '0') if (!query || query.trim().length === 0) { - return NextResponse.json({ error: 'Search query is required' }, { status: 400 }) + return NextResponse.json( + { error: 'Search query is required' }, + { status: 400 } + ) } - const lowerQuery = query.toLowerCase() + // Validate parameters + if (limit < 1 || limit > 100) { + return NextResponse.json( + { error: 'Limit must be between 1 and 100' }, + { status: 400 } + ) + } - // Search songs - SQLite uses LIKE for case-insensitive matching with LOWER() - const songs = await prisma.song.findMany({ - where: { - isPublic: true, - OR: [ - { - title: { - contains: lowerQuery, - }, - }, - { - description: { - contains: lowerQuery, - }, - }, - ], - }, - include: { - artist: { - select: { - id: true, - name: true, - slug: true, - verified: true, - }, - }, - album: { - select: { - id: true, - title: true, - slug: true, - coverUrl: true, - }, - }, - genres: { - include: { - genre: true, - }, - }, - }, - take: limit, - }) + if (offset < 0) { + return NextResponse.json( + { error: 'Offset must be non-negative' }, + { status: 400 } + ) + } - // Search artists - const artists = await prisma.artist.findMany({ - where: { - OR: [ - { - name: { - contains: lowerQuery, - }, - }, - { - bio: { - contains: lowerQuery, - }, - }, - ], - }, - include: { - _count: { - select: { - songs: true, - albums: true, - }, - }, - }, - take: limit, - }) + if (type && !['song', 'album', 'artist', 'playlist'].includes(type)) { + return NextResponse.json( + { error: 'Invalid type. Must be one of: song, album, artist, playlist' }, + { status: 400 } + ) + } - // Search albums - const albums = await prisma.album.findMany({ - where: { - OR: [ - { - title: { - contains: lowerQuery, - }, - }, - { - description: { - contains: lowerQuery, - }, - }, - ], - }, - include: { - artist: { - select: { - id: true, - name: true, - slug: true, - verified: true, - }, - }, - _count: { - select: { - songs: true, - }, - }, - }, - take: limit, - }) + // Perform search using the search index + const { results, total } = await searchEntities( + query.trim(), + type || undefined, + limit, + offset + ) - return NextResponse.json({ - songs, - artists, - albums, - }) + // Group results by entity type for easier frontend consumption + const groupedResults = { + songs: results.filter(r => r.entityType === 'song').map(r => r.entity), + albums: results.filter(r => r.entityType === 'album').map(r => r.entity), + artists: results.filter(r => r.entityType === 'artist').map(r => r.entity), + playlists: results.filter(r => r.entityType === 'playlist').map(r => r.entity), + } + + return NextResponse.json( + { + query, + type, + results: groupedResults, + total, + limit, + offset, + hasMore: offset + limit < total, + }, + { status: 200 } + ) } catch (error) { - console.error('Error searching:', error) - return NextResponse.json({ error: 'Failed to perform search' }, { status: 500 }) + console.error('Search error:', error) + return NextResponse.json( + { error: 'Failed to perform search' }, + { status: 500 } + ) } } diff --git a/app/api/search/suggestions/route.ts b/app/api/search/suggestions/route.ts new file mode 100644 index 0000000..a782e4d --- /dev/null +++ b/app/api/search/suggestions/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getSearchSuggestions } from '@/lib/search' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const query = searchParams.get('q') + const limit = parseInt(searchParams.get('limit') || '10') + + if (!query || query.trim().length === 0) { + return NextResponse.json( + { error: 'Search query is required' }, + { status: 400 } + ) + } + + // Validate limit + if (limit < 1 || limit > 50) { + return NextResponse.json( + { error: 'Limit must be between 1 and 50' }, + { status: 400 } + ) + } + + const suggestions = await getSearchSuggestions(query.trim(), limit) + + return NextResponse.json( + { + query, + suggestions, + }, + { status: 200 } + ) + } catch (error) { + console.error('Search suggestions error:', error) + return NextResponse.json( + { error: 'Failed to get search suggestions' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/share/[token]/click/route.ts b/app/api/share/[token]/click/route.ts deleted file mode 100644 index 52e699b..0000000 --- a/app/api/share/[token]/click/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import type { TrackShareClickResponse, ApiError } from '@/types/api-types'; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ token: string }> } -) { - try { - const { token } = await params; - - const share = await prisma.share.update({ - where: { token }, - data: { clickCount: { increment: 1 } }, - }); - - const response: TrackShareClickResponse = { - success: true, - clickCount: share.clickCount, - }; - - return NextResponse.json(response); - } catch (error) { - return NextResponse.json({ error: 'Share not found' }, { status: 404 }); - } -} diff --git a/app/api/share/[token]/route.ts b/app/api/share/[token]/route.ts deleted file mode 100644 index 0386fb5..0000000 --- a/app/api/share/[token]/route.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import type { ResolveShareResponse, SongShareContent, PlaylistShareContent, AlbumShareContent, ApiError, ShareType } from '@/types/api-types'; - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ token: string }> } -) { - try { - const { token } = await params; - - const share = await prisma.share.findUnique({ - where: { token }, - }); - - if (!share) { - return NextResponse.json( - { error: 'Share not found' }, - { status: 404 } - ); - } - - let content: SongShareContent | PlaylistShareContent | AlbumShareContent; - - if (share.type === 'SONG') { - const song = await prisma.song.findUnique({ - where: { id: share.targetId }, - include: { artist: true }, - }); - if (!song) { - return NextResponse.json({ error: 'Content not found' }, { status: 404 }); - } - content = { - id: song.id, - title: song.title, - duration: song.duration, - coverArtUrl: song.coverUrl || '', - fileUrl: song.audioUrl, - artist: { id: song.artist.id, stage_name: song.artist.name }, - }; - } else if (share.type === 'PLAYLIST') { - const playlist = await prisma.playlist.findUnique({ - where: { id: share.targetId }, - include: { - user: true, - songs: { include: { song: { include: { artist: true } } } } - }, - }); - if (!playlist) { - return NextResponse.json({ error: 'Content not found' }, { status: 404 }); - } - content = { - id: playlist.id, - name: playlist.title, - description: playlist.description || '', - coverImageUrl: playlist.coverUrl || '', - songCount: playlist.songs.length, - curator: { id: playlist.user.id, name: playlist.user.displayName || playlist.user.username }, - songs: playlist.songs.map(ps => ({ - id: ps.song.id, - title: ps.song.title, - duration: ps.song.duration, - cover_art_url: ps.song.coverUrl || '', - play_count: ps.song.playCount, - })), - }; - } else { - const album = await prisma.album.findUnique({ - where: { id: share.targetId }, - include: { artist: true, songs: true }, - }); - if (!album) { - return NextResponse.json({ error: 'Content not found' }, { status: 404 }); - } - content = { - id: album.id, - title: album.title, - description: album.description || '', - coverArtUrl: album.coverUrl || '', - releaseDate: album.releaseDate?.toISOString() || '', - artist: { id: album.artist.id, stage_name: album.artist.name }, - songs: album.songs.map(s => ({ - id: s.id, - title: s.title, - duration: s.duration, - cover_art_url: s.coverUrl || '', - play_count: s.playCount, - })), - }; - } - - const response: ResolveShareResponse = { - type: share.type as ShareType, - targetId: share.targetId, - content, - shareUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/s/${token}`, - }; - - return NextResponse.json(response); - } catch (error) { - console.error('Resolve share error:', error); - return NextResponse.json({ error: 'Failed to resolve share' }, { status: 500 }); - } -} diff --git a/app/api/share/album/[id]/route.ts b/app/api/share/album/[id]/route.ts deleted file mode 100644 index d3ddf88..0000000 --- a/app/api/share/album/[id]/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import { generateShareToken, buildShareUrl } from '@/lib/share'; -import type { CreateShareResponse, ApiError } from '@/types/api-types'; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const { id } = await params; - const body = await request.json().catch(() => ({})); - - // Verify album exists - const album = await prisma.album.findUnique({ - where: { id }, - }); - - if (!album) { - return NextResponse.json( - { error: 'Album not found' }, - { status: 404 } - ); - } - - // Generate unique token - const token = generateShareToken(); - - // Create share record - await prisma.share.create({ - data: { - type: 'ALBUM', - targetId: id, - token, - platform: body.platform || null, - }, - }); - - const response: CreateShareResponse = { - shareUrl: buildShareUrl(token), - token, - type: 'ALBUM', - }; - - return NextResponse.json(response); - } catch (error) { - console.error('Create album share error:', error); - return NextResponse.json( - { error: 'Failed to create share link' }, - { status: 500 } - ); - } -} diff --git a/app/api/share/playlist/[id]/route.ts b/app/api/share/playlist/[id]/route.ts deleted file mode 100644 index fbd5e06..0000000 --- a/app/api/share/playlist/[id]/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import { generateShareToken, buildShareUrl } from '@/lib/share'; -import type { CreateShareResponse, ApiError } from '@/types/api-types'; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const { id } = await params; - const body = await request.json().catch(() => ({})); - - // Verify playlist exists - const playlist = await prisma.playlist.findUnique({ - where: { id }, - }); - - if (!playlist) { - return NextResponse.json( - { error: 'Playlist not found' }, - { status: 404 } - ); - } - - // Generate unique token - const token = generateShareToken(); - - // Create share record - await prisma.share.create({ - data: { - type: 'PLAYLIST', - targetId: id, - token, - platform: body.platform || null, - }, - }); - - const response: CreateShareResponse = { - shareUrl: buildShareUrl(token), - token, - type: 'PLAYLIST', - }; - - return NextResponse.json(response); - } catch (error) { - console.error('Create playlist share error:', error); - return NextResponse.json( - { error: 'Failed to create share link' }, - { status: 500 } - ); - } -} diff --git a/app/api/share/song/[id]/route.ts b/app/api/share/song/[id]/route.ts deleted file mode 100644 index 1eaa895..0000000 --- a/app/api/share/song/[id]/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import { generateShareToken, buildShareUrl } from '@/lib/share'; -import type { CreateShareResponse, ApiError } from '@/types/api-types'; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const { id } = await params; - const body = await request.json().catch(() => ({})); - - // Verify song exists - const song = await prisma.song.findUnique({ - where: { id }, - }); - - if (!song) { - return NextResponse.json( - { error: 'Song not found' }, - { status: 404 } - ); - } - - // Generate unique token - const token = generateShareToken(); - - // Create share record - await prisma.share.create({ - data: { - type: 'SONG', - targetId: id, - token, - platform: body.platform || null, - }, - }); - - const response: CreateShareResponse = { - shareUrl: buildShareUrl(token), - token, - type: 'SONG', - }; - - return NextResponse.json(response); - } catch (error) { - console.error('Create song share error:', error); - return NextResponse.json( - { error: 'Failed to create share link' }, - { status: 500 } - ); - } -} diff --git a/app/api/upload/chunk/[uploadId]/[chunkIndex]/route.ts b/app/api/upload/chunk/[uploadId]/[chunkIndex]/route.ts new file mode 100644 index 0000000..113a6f5 --- /dev/null +++ b/app/api/upload/chunk/[uploadId]/[chunkIndex]/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getUploadSession, markChunkUploaded, generatePresignedUrl } from '@/lib/upload' +import { writeFile, mkdir } from 'fs/promises' +import { join } from 'path' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ uploadId: string; chunkIndex: string }> } +) { + try { + const user = await requireAuth() + const { uploadId, chunkIndex: chunkIndexStr } = await params + const chunkIndex = parseInt(chunkIndexStr) + + if (isNaN(chunkIndex)) { + return NextResponse.json( + { error: 'Invalid chunk index' }, + { status: 400 } + ) + } + + // Get upload session + const uploadSession = await getUploadSession(uploadId, user.id) + if (!uploadSession) { + return NextResponse.json( + { error: 'Upload session not found or expired' }, + { status: 404 } + ) + } + + if (uploadSession.status === 'completed') { + return NextResponse.json( + { error: 'Upload already completed' }, + { status: 400 } + ) + } + + if (chunkIndex < 0 || chunkIndex >= uploadSession.totalChunks) { + return NextResponse.json( + { error: 'Invalid chunk index' }, + { status: 400 } + ) + } + + // Get the chunk data from the request + const chunkData = await request.arrayBuffer() + const expectedSize = Math.min(uploadSession.chunkSize, uploadSession.fileSize - chunkIndex * uploadSession.chunkSize) + + if (chunkData.byteLength !== expectedSize) { + return NextResponse.json( + { error: `Invalid chunk size. Expected ${expectedSize}, got ${chunkData.byteLength}` }, + { status: 400 } + ) + } + + // In a real implementation, you would: + // 1. Upload to S3/CloudStorage using the presigned URL + // 2. Or store temporarily on disk/cloud storage + // For now, we'll simulate by storing in a temporary directory + + const tempDir = join(process.cwd(), 'temp', 'uploads', uploadId) + await mkdir(tempDir, { recursive: true }) + + const chunkPath = join(tempDir, `chunk-${chunkIndex}`) + await writeFile(chunkPath, Buffer.from(chunkData)) + + // Mark chunk as uploaded + const updatedSession = await markChunkUploaded(uploadId, chunkIndex) + + // Generate presigned URL for next chunk if not complete + let nextPresignedUrl = null + if (updatedSession.status !== 'completed' && chunkIndex + 1 < uploadSession.totalChunks) { + const { url } = await generatePresignedUrl( + `chunk-${chunkIndex + 1}-${uploadSession.fileName}`, + uploadSession.mimeType + ) + nextPresignedUrl = url + } + + return NextResponse.json( + { + success: true, + chunkIndex, + uploadedChunks: updatedSession.uploadedChunks, + totalChunks: updatedSession.totalChunks, + status: updatedSession.status, + nextPresignedUrl, + }, + { status: 200 } + ) + } catch (error) { + console.error('Chunk upload error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to upload chunk' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/upload/complete/[uploadId]/route.ts b/app/api/upload/complete/[uploadId]/route.ts new file mode 100644 index 0000000..880090a --- /dev/null +++ b/app/api/upload/complete/[uploadId]/route.ts @@ -0,0 +1,112 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getUploadSession, completeUploadSession } from '@/lib/upload' +import { prisma } from '@/lib/prisma' +import { readFile, writeFile, mkdir, rm } from 'fs/promises' +import { join } from 'path' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ uploadId: string }> } +) { + try { + const user = await requireAuth() + const { uploadId } = await params + + // Get upload session + const uploadSession = await getUploadSession(uploadId, user.id) + if (!uploadSession) { + return NextResponse.json( + { error: 'Upload session not found or expired' }, + { status: 404 } + ) + } + + if (uploadSession.status !== 'completed') { + return NextResponse.json( + { error: 'Upload not complete. All chunks must be uploaded first.' }, + { status: 400 } + ) + } + + // Combine chunks into a single file + const tempDir = join(process.cwd(), 'temp', 'uploads', uploadId) + const finalPath = join(process.cwd(), 'uploads', `${uploadId}-${uploadSession.fileName}`) + + // Ensure uploads directory exists + await mkdir(join(process.cwd(), 'uploads'), { recursive: true }) + + // Combine all chunks + const fileBuffer = Buffer.alloc(uploadSession.fileSize) + let offset = 0 + + for (let i = 0; i < uploadSession.totalChunks; i++) { + const chunkPath = join(tempDir, `chunk-${i}`) + const chunkData = await readFile(chunkPath) + chunkData.copy(fileBuffer, offset) + offset += chunkData.length + } + + // Write the complete file + await writeFile(finalPath, fileBuffer) + + // Create a database record for the uploaded file + const fileId = `file_${Date.now()}_${uploadId}` + + // In a real implementation, you would: + // 1. Upload to S3/CloudStorage + // 2. Store the URL/Key in the database + // 3. Process the file (transcode audio, generate thumbnails, etc.) + // For now, we'll create a simple record + + let createdRecord = null + + if (uploadSession.mimeType.startsWith('audio/')) { + // Create a song record + createdRecord = await prisma.song.create({ + data: { + artistId: user.id, // Assuming user is an artist + title: uploadSession.fileName.replace(/\.[^/.]+$/, ''), // Remove extension + slug: uploadSession.fileName.replace(/\.[^/.]+$/, '').toLowerCase().replace(/\s+/g, '-'), + audioUrl: `/uploads/${uploadId}-${uploadSession.fileName}`, + duration: 0, // Would be determined during processing + isPublic: false, // Default to private until processed + }, + }) + } else if (uploadSession.mimeType.startsWith('image/')) { + // Could create an image record or associate with artist/album + // For now, just store the file reference + } + + // Mark upload session as complete with file ID + const completedSession = await completeUploadSession(uploadId, fileId) + + // Clean up temporary chunks + await rm(tempDir, { recursive: true, force: true }) + + return NextResponse.json( + { + success: true, + fileId, + fileName: uploadSession.fileName, + fileSize: uploadSession.fileSize, + mimeType: uploadSession.mimeType, + record: createdRecord, + uploadSession: completedSession, + }, + { status: 200 } + ) + } catch (error) { + console.error('Upload complete error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to complete upload' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/upload/init/route.ts b/app/api/upload/init/route.ts new file mode 100644 index 0000000..6b2784c --- /dev/null +++ b/app/api/upload/init/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { createUploadSession, generatePresignedUrl, validateFileType, validateFileSize } from '@/lib/upload' + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + const body = await request.json() + const { fileName, fileSize, mimeType, chunkSize, metadata } = body + + // Validate required fields + if (!fileName || !fileSize || !mimeType) { + return NextResponse.json( + { error: 'Missing required fields: fileName, fileSize, mimeType' }, + { status: 400 } + ) + } + + // Validate file size (max 500MB) + const maxSize = 500 * 1024 * 1024 // 500MB + if (!validateFileSize(fileSize, maxSize)) { + return NextResponse.json( + { error: `File size exceeds maximum allowed size of ${maxSize / 1024 / 1024}MB` }, + { status: 400 } + ) + } + + // Validate allowed file types + const allowedTypes = [ + 'audio/*', + 'image/*', + ] + if (!validateFileType(mimeType, allowedTypes)) { + return NextResponse.json( + { error: 'File type not allowed' }, + { status: 400 } + ) + } + + // Create upload session + const uploadSession = await createUploadSession( + user.id, + fileName, + fileSize, + mimeType, + chunkSize, + metadata + ) + + // Generate first chunk presigned URL + const { url, key } = await generatePresignedUrl( + `chunk-0-${fileName}`, + mimeType + ) + + return NextResponse.json( + { + uploadSession, + presignedUrl: url, + uploadKey: key, + chunkSize: uploadSession.chunkSize, + totalChunks: uploadSession.totalChunks, + }, + { status: 201 } + ) + } catch (error) { + console.error('Upload init error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to initialize upload' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/upload/presigned-url/route.ts b/app/api/upload/presigned-url/route.ts new file mode 100644 index 0000000..73dd5e1 --- /dev/null +++ b/app/api/upload/presigned-url/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { generatePresignedUrl } from '@/lib/upload' + +export async function GET(request: NextRequest) { + try { + const user = await requireAuth() + const { searchParams } = new URL(request.url) + + const fileName = searchParams.get('fileName') + const mimeType = searchParams.get('mimeType') + const expiresIn = parseInt(searchParams.get('expiresIn') || '3600') + + if (!fileName || !mimeType) { + return NextResponse.json( + { error: 'fileName and mimeType are required' }, + { status: 400 } + ) + } + + // Generate presigned URL + const { url, key } = await generatePresignedUrl(fileName, mimeType, expiresIn) + + return NextResponse.json( + { + presignedUrl: url, + key, + expiresIn, + }, + { status: 200 } + ) + } catch (error) { + console.error('Generate presigned URL error:', error) + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + return NextResponse.json( + { error: 'Failed to generate presigned URL' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/components/AudioPlayer.tsx b/app/components/AudioPlayer.tsx new file mode 100644 index 0000000..f985de2 --- /dev/null +++ b/app/components/AudioPlayer.tsx @@ -0,0 +1,404 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import type { Queue, PlayHistory } from '../types/api'; +import { API_PATHS } from '../types/api'; + +interface AudioPlayerProps { + currentSong?: { + id: string; + title: string; + artist: string; + duration: number; + url: string; + }; + queue?: Queue; + isPlaying?: boolean; + volume?: number; + currentTime?: number; + onPlay?: () => void; + onPause?: () => void; + onNext?: () => void; + onPrevious?: () => void; + onSeek?: (time: number) => void; + onVolumeChange?: (volume: number) => void; + onQueueUpdate?: (queue: Queue) => void; +} + +type RepeatMode = 'none' | 'one' | 'all'; + +export default function AudioPlayer({ + currentSong, + queue, + isPlaying = false, + volume = 0.7, + currentTime = 0, + onPlay, + onPause, + onNext, + onPrevious, + onSeek, + onVolumeChange, + onQueueUpdate, +}: AudioPlayerProps) { + const [localVolume, setLocalVolume] = useState(volume); + const [localCurrentTime, setLocalCurrentTime] = useState(currentTime); + const [repeatMode, setRepeatMode] = useState('none'); + const [isShuffled, setIsShuffled] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [showQueue, setShowQueue] = useState(false); + + const audioRef = useRef(null); + const progressBarRef = useRef(null); + + // Sync with props + useEffect(() => { + setLocalVolume(volume); + }, [volume]); + + useEffect(() => { + setLocalCurrentTime(currentTime); + }, [currentTime]); + + // Format time helper + const formatTime = (seconds: number): string => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + // API calls with proper error handling + const handlePlay = async () => { + try { + const response = await fetch(API_PATHS.PLAYER_PLAY, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!response.ok) { + throw new Error('Failed to play'); + } + + onPlay?.(); + } catch (error) { + console.error('Play error:', error); + } + }; + + const handlePause = async () => { + try { + const response = await fetch(API_PATHS.PLAYER_PAUSE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!response.ok) { + throw new Error('Failed to pause'); + } + + onPause?.(); + } catch (error) { + console.error('Pause error:', error); + } + }; + + const handleNext = async () => { + try { + const response = await fetch(API_PATHS.PLAYER_NEXT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!response.ok) { + throw new Error('Failed to play next'); + } + + onNext?.(); + } catch (error) { + console.error('Next error:', error); + } + }; + + const handlePrevious = async () => { + try { + const response = await fetch(API_PATHS.PLAYER_PREVIOUS, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!response.ok) { + throw new Error('Failed to play previous'); + } + + onPrevious?.(); + } catch (error) { + console.error('Previous error:', error); + } + }; + + const handleVolumeChange = async (newVolume: number) => { + try { + setLocalVolume(newVolume); + setIsMuted(newVolume === 0); + onVolumeChange?.(newVolume); + + // Volume change would typically be handled client-side + // But we could persist it to user preferences + } catch (error) { + console.error('Volume change error:', error); + } + }; + + const handleSeek = (time: number) => { + setLocalCurrentTime(time); + onSeek?.(time); + + if (audioRef.current) { + audioRef.current.currentTime = time; + } + }; + + const handleProgressClick = (e: React.MouseEvent) => { + if (!progressBarRef.current || !currentSong) return; + + const rect = progressBarRef.current.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + const newTime = percent * currentSong.duration; + + handleSeek(newTime); + }; + + const toggleRepeat = () => { + const modes: RepeatMode[] = ['none', 'one', 'all']; + const currentIndex = modes.indexOf(repeatMode); + const nextMode = modes[(currentIndex + 1) % modes.length]; + setRepeatMode(nextMode); + }; + + const toggleShuffle = () => { + setIsShuffled(!isShuffled); + }; + + const toggleMute = () => { + if (isMuted) { + handleVolumeChange(volume); + } else { + handleVolumeChange(0); + } + }; + + const fetchQueue = async () => { + try { + const response = await fetch(API_PATHS.PLAYER_QUEUE); + if (response.ok) { + const queueData: Queue = await response.json(); + onQueueUpdate?.(queueData); + } + } catch (error) { + console.error('Failed to fetch queue:', error); + } + }; + + // Progress percentage + const progressPercent = currentSong + ? (localCurrentTime / currentSong.duration) * 100 + : 0; + + return ( +
+ {/* Audio element for actual playback */} + {currentSong && ( +