globe-server 0.0.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. globe_server/__init__.py +5 -0
  2. globe_server/api/__init__.py +22 -0
  3. globe_server/api/media.py +191 -0
  4. globe_server/api/motor.py +44 -0
  5. globe_server/api/network.py +73 -0
  6. globe_server/api/playback.py +59 -0
  7. globe_server/api/playlist.py +182 -0
  8. globe_server/api/settings.py +67 -0
  9. globe_server/api/status.py +62 -0
  10. globe_server/api/update.py +254 -0
  11. globe_server/api/version.py +39 -0
  12. globe_server/api/websocket.py +169 -0
  13. globe_server/config.py +83 -0
  14. globe_server/db/__init__.py +5 -0
  15. globe_server/db/broadcast_queue.py +106 -0
  16. globe_server/db/database.py +214 -0
  17. globe_server/db/db_events.py +75 -0
  18. globe_server/db/events.py +115 -0
  19. globe_server/db/orm.py +154 -0
  20. globe_server/db/schemas.py +199 -0
  21. globe_server/hardware/esp32_client.py +293 -0
  22. globe_server/hardware/server_hardware.py +89 -0
  23. globe_server/main.py +274 -0
  24. globe_server/models/__init__.py +5 -0
  25. globe_server/models/status.py +31 -0
  26. globe_server/network_manager/mdns_manager.py +123 -0
  27. globe_server/network_manager/models.py +61 -0
  28. globe_server/network_manager/network_fsm.py +595 -0
  29. globe_server/network_manager/network_service.py +75 -0
  30. globe_server/network_manager/platform/__init__.py +51 -0
  31. globe_server/network_manager/platform/base_network.py +52 -0
  32. globe_server/network_manager/platform/linux_network.py +184 -0
  33. globe_server/network_manager/platform/windows_network.py +288 -0
  34. globe_server/playback/event_emitter.py +51 -0
  35. globe_server/playback/media_player.py +177 -0
  36. globe_server/playback/media_services/__init__.py +11 -0
  37. globe_server/playback/media_services/earth_viz_service.py +111 -0
  38. globe_server/playback/media_services/media_window.py +308 -0
  39. globe_server/playback/media_services/vlc_service.py +194 -0
  40. globe_server/playback/playback_context.py +369 -0
  41. globe_server/playback/playback_fsm.py +513 -0
  42. globe_server/playback/playback_service.py +57 -0
  43. globe_server/playback/playback_state.py +100 -0
  44. globe_server/playback/playback_tracker.py +167 -0
  45. globe_server/setup.py +170 -0
  46. globe_server/static/index-2RLu-Oe0.js +73 -0
  47. globe_server/static/index-DX6Xxgpy.css +1 -0
  48. globe_server/static/index.html +14 -0
  49. globe_server/static/vite.svg +1 -0
  50. globe_server/tools/espota.py +325 -0
  51. globe_server/utils/__init__.py +0 -0
  52. globe_server/utils/eventbus.py +75 -0
  53. globe_server/utils/files.py +193 -0
  54. globe_server/utils/uart.py +39 -0
  55. globe_server-0.0.30.dist-info/METADATA +259 -0
  56. globe_server-0.0.30.dist-info/RECORD +60 -0
  57. globe_server-0.0.30.dist-info/WHEEL +5 -0
  58. globe_server-0.0.30.dist-info/entry_points.txt +3 -0
  59. globe_server-0.0.30.dist-info/licenses/LICENSE +21 -0
  60. globe_server-0.0.30.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """Globe Server Backend Package"""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Edward Catley"
5
+ __description__ = "Globe Server Backend - Webserver control of Globe"
@@ -0,0 +1,22 @@
1
+ from fastapi import APIRouter
2
+ from .status import router as status_router
3
+ from .media import router as media_router
4
+ from .motor import router as motor_router
5
+ from .playlist import router as playlist_router
6
+ from .playback import router as playback_router
7
+ from .update import router as update_router
8
+ from .settings import router as settings_router
9
+ from .version import router as version_router
10
+
11
+
12
+ api_router = APIRouter()
13
+
14
+ api_router.include_router(status_router)
15
+ api_router.include_router(media_router)
16
+ api_router.include_router(motor_router)
17
+ api_router.include_router(playlist_router)
18
+ api_router.include_router(playback_router)
19
+ api_router.include_router(update_router)
20
+ api_router.include_router(settings_router)
21
+ api_router.include_router(version_router)
22
+
@@ -0,0 +1,191 @@
1
+ # app/api/media.py
2
+ from fastapi import APIRouter, Depends, HTTPException, Header, status, UploadFile, File, Form, Query
3
+ from typing import List
4
+ from sqlalchemy.orm import Session
5
+ from globe_server.db.orm import Media
6
+ from globe_server.db.database import create, update, delete, get_by_id, get_all
7
+ from globe_server.db.schemas import MediaCreate, MediaRead, MediaUpdate, PlanetOptions
8
+ from globe_server.utils import files
9
+ from globe_server import config
10
+ import os
11
+ import logging
12
+ import json
13
+
14
+ router = APIRouter(prefix="/media", tags=["media"])
15
+
16
+ @router.get("/files")
17
+ def list_media_files(type: str = Query(..., regex="^(video|image)$")):
18
+ """List available media files on the server for a given type."""
19
+ try:
20
+ file_list = files.list_media_files(type)
21
+ return {"files": file_list}
22
+ except Exception as e:
23
+ logging.error(f"Error listing media files: {str(e)}", exc_info=True)
24
+ raise HTTPException(
25
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
26
+ detail=str(e)
27
+ )
28
+
29
+ @router.post("/upload/")
30
+ async def upload_file(
31
+ file: UploadFile = File(...),
32
+ media_type: str = Form(...),
33
+ ):
34
+ try:
35
+ if media_type == "image":
36
+ upload_directory = os.path.join(config.GLOBE_MEDIA_DIRECTORY, "Images")
37
+ elif media_type == "video":
38
+ upload_directory = os.path.join(config.GLOBE_MEDIA_DIRECTORY, "Videos")
39
+ else:
40
+ raise HTTPException(
41
+ status_code=status.HTTP_400_BAD_REQUEST,
42
+ detail=f"Invalid media type: {media_type}. Allowed types: image, video"
43
+ )
44
+
45
+ file_path = await files.save_uploaded_file(file, upload_directory)
46
+ return {"filepath": file_path}
47
+ except ValueError as e:
48
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
49
+ except HTTPException as e:
50
+ raise e
51
+ except Exception as e:
52
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
53
+
54
+ @router.get("/", response_model=List[MediaRead])
55
+ def get_all_media():
56
+ try:
57
+ media_items = get_all(Media)
58
+ return media_items
59
+ except Exception as e:
60
+ logging.error(f"Error getting all media: {e}")
61
+ raise HTTPException(
62
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
63
+ )
64
+
65
+ @router.post("/", response_model=MediaRead)
66
+ def create_media(media: MediaCreate):
67
+ try:
68
+ # Only validate filepath for image and video types
69
+ if media.type in ("image", "video"):
70
+ if not media.filepath or not os.path.exists(media.filepath):
71
+ raise HTTPException(
72
+ status_code=status.HTTP_400_BAD_REQUEST,
73
+ detail="Filepath is required and must exist for image and video types",
74
+ )
75
+
76
+ # Serialize planet_options to JSON string if it exists
77
+ planet_options_json = None
78
+ if media.planet_options:
79
+ if isinstance(media.planet_options, dict):
80
+ planet_options_json = json.dumps(media.planet_options)
81
+ else:
82
+ planet_options_json = media.planet_options.model_dump_json(exclude_none=True)
83
+
84
+ # Create new media item
85
+ db_media = Media(
86
+ type=media.type,
87
+ name=media.name,
88
+ filepath=media.filepath,
89
+ display_duration=media.display_duration,
90
+ planet_options=planet_options_json
91
+ )
92
+
93
+ # Use CRUD function that handles broadcasting
94
+ db_media = create(db_media)
95
+
96
+ return db_media
97
+ except Exception as e:
98
+ logging.error(f"Error creating media: {e}")
99
+ #db.rollback()
100
+ raise HTTPException(
101
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
102
+ )
103
+
104
+ @router.get("/{media_id}", response_model=MediaRead)
105
+ def get_media(media_id: int):
106
+ try:
107
+ media = get_by_id(Media, media_id)
108
+ if media is None:
109
+ raise HTTPException(
110
+ status_code=status.HTTP_404_NOT_FOUND, detail="Media item not found"
111
+ )
112
+ return media
113
+ except HTTPException:
114
+ raise
115
+ except Exception as e:
116
+ logging.error(f"Error getting media: {e}")
117
+ raise HTTPException(
118
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
119
+ )
120
+
121
+ @router.put("/{media_id}", response_model=MediaRead)
122
+ def update_media(media_id: int, media_update: MediaUpdate):
123
+ try:
124
+ # Get the existing media item
125
+ db_media = get_by_id(Media, media_id)
126
+ if db_media is None:
127
+ raise HTTPException(
128
+ status_code=status.HTTP_404_NOT_FOUND, detail="Media item not found"
129
+ )
130
+
131
+ # Update the fields that are provided
132
+ update_data = media_update.model_dump(exclude_unset=True)
133
+
134
+ # Handle planet_options separately if it exists
135
+ if 'planet_options' in update_data and update_data['planet_options'] is not None:
136
+ if isinstance(update_data['planet_options'], dict):
137
+ db_media.planet_options = json.dumps(update_data['planet_options'])
138
+ else:
139
+ db_media.planet_options = update_data['planet_options'].model_dump_json(exclude_none=True)
140
+ del update_data['planet_options']
141
+
142
+ # Update all other fields
143
+ for key, value in update_data.items():
144
+ if value is not None: # Only update non-None values
145
+ setattr(db_media, key, value)
146
+
147
+ # Use CRUD function that handles broadcasting
148
+ db_media = update(db_media)
149
+
150
+ return db_media
151
+ except HTTPException:
152
+ raise
153
+ except Exception as e:
154
+ logging.error(f"Error updating media: {e}")
155
+ raise HTTPException(
156
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
157
+ )
158
+
159
+ @router.delete("/{media_id}")
160
+ def delete_media(media_id: int):
161
+ try:
162
+ # Get the media item
163
+ db_media = get_by_id(Media, media_id)
164
+ if db_media is None:
165
+ raise HTTPException(
166
+ status_code=status.HTTP_404_NOT_FOUND, detail="Media item not found"
167
+ )
168
+
169
+ # Store filepath and type before deletion
170
+ filepath = db_media.filepath
171
+ media_type = db_media.type
172
+
173
+ # Delete the media item from the database using CRUD function
174
+ delete(db_media)
175
+
176
+ # Delete the file from the file system if it's an image or video
177
+ if media_type in ("image", "video") and filepath:
178
+ try:
179
+ files.delete_file(filepath)
180
+ except Exception as e:
181
+ # Log the error but don't raise an exception, as the media item has already been deleted
182
+ logging.warning(f"Error deleting file: {e}")
183
+
184
+ return {"message": "Media item deleted successfully"}
185
+ except HTTPException:
186
+ raise
187
+ except Exception as e:
188
+ logging.error(f"Error deleting media: {e}")
189
+ raise HTTPException(
190
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
191
+ )
@@ -0,0 +1,44 @@
1
+ # app/api/motor.py
2
+
3
+ from fastapi import APIRouter, HTTPException, status
4
+ from globe_server import config
5
+ from globe_server.hardware.esp32_client import esp32_client, ESP32Error
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ router = APIRouter(prefix="/motor", tags=["motor"])
11
+
12
+ @router.post("/start")
13
+ async def start_motor():
14
+ try:
15
+ await esp32_client.set_motor_speed(config.MOTOR_RPM)
16
+ return {"message": "Motor started successfully"}
17
+ except ESP32Error as e:
18
+ raise HTTPException(
19
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
20
+ detail=str(e)
21
+ )
22
+
23
+ @router.post("/stop")
24
+ async def stop_motor():
25
+ try:
26
+ await esp32_client.set_motor_speed(0)
27
+ return {"message": "Motor stopped successfully"}
28
+ except ESP32Error as e:
29
+ raise HTTPException(
30
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
31
+ detail=str(e)
32
+ )
33
+
34
+ @router.get("/status")
35
+ async def get_motor_status():
36
+ """Get the current motor status."""
37
+ try:
38
+ return await esp32_client.get_status()
39
+ except ESP32Error as e:
40
+ logger.error(f"Error getting motor status: {e}")
41
+ raise HTTPException(
42
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
43
+ detail=str(e)
44
+ )
@@ -0,0 +1,73 @@
1
+ from fastapi import APIRouter, Depends, HTTPException, Header, status
2
+ from pydantic import BaseModel
3
+ from typing import List, Optional
4
+ from globe_server.network_manager.network_service import network_manager as state_machine, connect_to_wifi, scan_available_networks
5
+ from globe_server.network_manager.models import NetworkInfo as NetworkInfoModel
6
+ from globe_server.network_manager.platform import get_current_connection_info
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ router = APIRouter(prefix="/network", tags=["network"])
12
+
13
+ class WiFiConfig(BaseModel):
14
+ ssid: str
15
+ password: str
16
+
17
+ # Use our model directly
18
+ class WiFiNetwork(BaseModel):
19
+ ssid: str
20
+ signal_strength: int
21
+ security: str
22
+
23
+ @classmethod
24
+ def from_model(cls, model: NetworkInfoModel) -> 'WiFiNetwork':
25
+ return cls(
26
+ ssid=model.ssid,
27
+ signal_strength=model.signal_strength,
28
+ security=model.security
29
+ )
30
+
31
+ class WiFiStatus(BaseModel):
32
+ status: str # FSM state (idle, connecting, connected, etc.)
33
+ current_ssid: str | None # From OS
34
+ ip_address: str | None # From OS
35
+ esp32_connected: bool # From FSM
36
+ error: str | None = None # From FSM
37
+
38
+
39
+ @router.post("/wifi")
40
+ async def configure_wifi(wifi_config: WiFiConfig):
41
+ """Initiate WiFi connection (async operation)."""
42
+ success = await connect_to_wifi(wifi_config.ssid, wifi_config.password)
43
+ if not success:
44
+ raise HTTPException(
45
+ status_code=status.HTTP_400_BAD_REQUEST,
46
+ detail="Failed to initiate WiFi connection"
47
+ )
48
+
49
+ return {"message": f"Connection to {wifi_config.ssid} initiated"}
50
+
51
+
52
+ @router.get("/wifi/status", response_model=WiFiStatus)
53
+ async def get_wifi_status():
54
+ """Get current WiFi connection status."""
55
+ # Query OS for real-time connection info
56
+ conn_info = await get_current_connection_info()
57
+
58
+ return WiFiStatus(
59
+ status=state_machine.context.current_state.value,
60
+ current_ssid=conn_info.ssid,
61
+ ip_address=conn_info.ip_address,
62
+ esp32_connected=state_machine.context.esp32_connected,
63
+ error=state_machine.context.error_message
64
+ )
65
+
66
+ @router.get("/wifi/scan", response_model=dict)
67
+ async def scan_networks():
68
+ """Scan for available WiFi networks."""
69
+ network_models = await scan_available_networks()
70
+ networks = [WiFiNetwork.from_model(model) for model in network_models]
71
+ return {"networks": networks}
72
+
73
+
@@ -0,0 +1,59 @@
1
+ from fastapi import APIRouter, HTTPException, status
2
+ from fastapi.responses import JSONResponse
3
+ import logging
4
+ import asyncio
5
+ from globe_server.playback import playback_service
6
+ import time
7
+ import vlc
8
+ import subprocess
9
+ from globe_server import config
10
+ import os
11
+ import json
12
+ from typing import List, Optional, Dict, Any
13
+
14
+ # Create router for REST endpoints
15
+ router = APIRouter(prefix="/playback", tags=["playback"])
16
+
17
+ # Configure logging
18
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
19
+
20
+ # =====================================================================
21
+ # API ENDPOINTS
22
+ # =====================================================================
23
+
24
+ @router.post("/play")
25
+ async def play_playlist():
26
+ """Start playing the playlist or resume if paused."""
27
+ try:
28
+ if await playback_service.play():
29
+ return JSONResponse({"status": "success", "message": "Playback started/resumed"})
30
+ else:
31
+ return JSONResponse({"status": "error", "message": "Failed to start playback"})
32
+ except Exception as e:
33
+ logging.error(f"Error starting playback: {e}")
34
+ return JSONResponse({"status": "error", "message": str(e)})
35
+
36
+ @router.post("/stop")
37
+ async def stop_playback():
38
+ """Stop the current playback."""
39
+ try:
40
+ if await playback_service.stop():
41
+ return JSONResponse({"status": "success", "message": "Playback stopped"})
42
+ else:
43
+ return JSONResponse({"status": "error", "message": "Failed to stop playback"})
44
+ except Exception as e:
45
+ logging.error(f"Error stopping playback: {e}")
46
+ return JSONResponse({"status": "error", "message": str(e)})
47
+
48
+ @router.post("/pause")
49
+ async def pause_item():
50
+ """Pauses the currently playing media item."""
51
+ try:
52
+ if await playback_service.pause():
53
+ return JSONResponse({"status": "success", "message": "Playback paused"})
54
+ else:
55
+ return JSONResponse({"status": "error", "message": "Failed to pause playback"})
56
+ except Exception as e:
57
+ logging.error(f"Error pausing playback: {e}")
58
+ return JSONResponse({"status": "error", "message": str(e)})
59
+
@@ -0,0 +1,182 @@
1
+ # app/api/playlist.py
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, status
4
+ from typing import List, Optional
5
+ from sqlalchemy.orm import Session
6
+
7
+ from globe_server.db.orm import Media, PlaylistItem
8
+ from globe_server.db.database import create, update, delete, get_by_id, get_all, get_max_position, get_all_ordered_by_position, update_playlist_position
9
+ from globe_server.db.schemas import PlaylistItemCreate, PlaylistItemUpdate, PlaylistItemRead
10
+ import os
11
+ import logging
12
+
13
+ # Create router for REST endpoints
14
+ router = APIRouter(prefix="/playlist", tags=["playlist"])
15
+
16
+ # Helper functions for consistent data handling using SQLAlchemy ORM
17
+
18
+ def _get_playlist_item_with_media(playlist_id: int) -> Optional[PlaylistItem]:
19
+ """Fetch a single playlist item with its media data using ORM"""
20
+ return get_by_id(PlaylistItem, playlist_id)
21
+
22
+ # =====================================================================
23
+ # HELPER FUNCTIONS
24
+ # =====================================================================
25
+
26
+ def get_playlist_items() -> List[PlaylistItem]:
27
+ """
28
+ Get all playlist items ordered by position.
29
+ """
30
+ return get_all_ordered_by_position()
31
+
32
+ # =====================================================================
33
+ # API ENDPOINTS
34
+ # =====================================================================
35
+
36
+ @router.get("/", response_model=List[PlaylistItemRead])
37
+ async def get_all_playlist_items():
38
+ """
39
+ Get all playlist items with complete media details.
40
+
41
+ This endpoint is for direct REST API access to playlist data.
42
+ Real-time updates are now handled by the status WebSocket.
43
+ """
44
+ return get_playlist_items()
45
+
46
+ @router.post("/", response_model=PlaylistItemRead)
47
+ async def create_playlist_item(playlist_item: PlaylistItemCreate):
48
+ logging.info(f"=== CREATE PLAYLIST ITEM CALLED ===")
49
+ logging.info(f"Received playlist_item: {playlist_item}")
50
+
51
+ try:
52
+ # Check if the media_id exists
53
+ logging.info(f"Checking if media_id {playlist_item.media_id} exists...")
54
+ media = get_by_id(Media, playlist_item.media_id)
55
+ if media is None:
56
+ logging.error(f"Media ID {playlist_item.media_id} not found")
57
+ raise HTTPException(
58
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid media_id"
59
+ )
60
+ logging.info(f"Media found: {media.name}")
61
+
62
+ # Get the next available position
63
+ logging.info("Getting next position...")
64
+ next_position = get_max_position() + 1
65
+ logging.info(f"Next position: {next_position}")
66
+
67
+ # Create new playlist item
68
+ logging.info("Creating PlaylistItem object...")
69
+ new_item = PlaylistItem(
70
+ media_id=playlist_item.media_id,
71
+ position=next_position,
72
+ status="unplayed",
73
+ elapsed_time=0
74
+ )
75
+
76
+ # Use CRUD function that handles broadcasting
77
+ logging.info("Calling create() to save to database...")
78
+ new_item = create(new_item)
79
+ logging.info(f"Successfully created playlist item with ID: {new_item.id}")
80
+
81
+ return new_item
82
+ except HTTPException:
83
+ raise
84
+ except Exception as e:
85
+ logging.error(f"Error creating playlist item: {e}", exc_info=True)
86
+ raise HTTPException(
87
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
88
+ )
89
+
90
+ @router.get("/{playlist_id}", response_model=PlaylistItemRead)
91
+ async def get_playlist_item(playlist_id: int):
92
+ """
93
+ NOTE: This endpoint is not currently used by the frontend.
94
+ The frontend primarily receives playlist data via WebSockets.
95
+
96
+ This endpoint is kept for API completeness, external tools,
97
+ and as a fallback mechanism.
98
+ """
99
+ playlist_item = _get_playlist_item_with_media(playlist_id)
100
+ if playlist_item is None:
101
+ raise HTTPException(
102
+ status_code=status.HTTP_404_NOT_FOUND, detail="Playlist item not found"
103
+ )
104
+
105
+ return playlist_item
106
+
107
+ @router.put("/{playlist_id}", response_model=PlaylistItemRead)
108
+ async def update_playlist_item(playlist_id: int, playlist_item_update: PlaylistItemUpdate):
109
+ try:
110
+ # Check if the item exists
111
+ db_item = get_by_id(PlaylistItem, playlist_id)
112
+ if db_item is None:
113
+ raise HTTPException(
114
+ status_code=status.HTTP_404_NOT_FOUND, detail="Playlist item not found"
115
+ )
116
+
117
+ # Check if media_id exists if it's being updated
118
+ if playlist_item_update.media_id is not None:
119
+ media = get_by_id(Media, playlist_item_update.media_id)
120
+ if media is None:
121
+ raise HTTPException(
122
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid media_id"
123
+ )
124
+
125
+ # Update the playlist item attributes
126
+ update_data = playlist_item_update.model_dump(exclude_unset=True)
127
+
128
+ # If position is being updated, use database function to handle shifts
129
+ if 'position' in update_data:
130
+ new_position = update_data['position']
131
+ logging.info(f"Updating item {playlist_id} position to {new_position}")
132
+
133
+ # Database function handles shifting other items and updating this one
134
+ update_playlist_position(playlist_id, new_position)
135
+
136
+ # Get the updated item to return
137
+ db_item = get_by_id(PlaylistItem, playlist_id)
138
+ else:
139
+ # No position change, just update other attributes
140
+ for key, value in update_data.items():
141
+ setattr(db_item, key, value)
142
+ db_item = update(db_item)
143
+
144
+ # Broadcasting is now handled by the CRUD functions
145
+ return db_item
146
+ except HTTPException:
147
+ raise
148
+ except Exception as e:
149
+ logging.error(f"Error updating playlist item: {e}")
150
+ raise HTTPException(
151
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
152
+ )
153
+
154
+ @router.delete("/{playlist_id}")
155
+ async def delete_playlist_item(playlist_id: int):
156
+ try:
157
+ # Check if the item exists
158
+ db_item = get_by_id(PlaylistItem, playlist_id)
159
+ if db_item is None:
160
+ raise HTTPException(
161
+ status_code=status.HTTP_404_NOT_FOUND, detail="Playlist item not found"
162
+ )
163
+
164
+ # Delete the item using CRUD function
165
+ delete(db_item)
166
+
167
+ # Reset positions for remaining items
168
+ all_items = get_all_ordered_by_position()
169
+ for i, item in enumerate(all_items, 1):
170
+ if item.position != i:
171
+ item.position = i
172
+ update(item)
173
+
174
+ # Broadcasting is now handled by the CRUD functions
175
+ return {"message": "Playlist item deleted successfully"}
176
+ except HTTPException:
177
+ raise
178
+ except Exception as e:
179
+ logging.error(f"Error deleting playlist item: {e}")
180
+ raise HTTPException(
181
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
182
+ )
@@ -0,0 +1,67 @@
1
+ # app/api/settings.py
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, status
4
+ from sqlalchemy.orm import Session
5
+
6
+ from globe_server.db.orm import Settings
7
+ from globe_server.db.schemas import SettingsRead, SettingsUpdate
8
+ from globe_server.db.database import get_singleton, update, get_all
9
+ from globe_server.utils import uart # Import the uart module
10
+ from globe_server import config
11
+ import logging
12
+
13
+ router = APIRouter(prefix="/settings", tags=["settings"])
14
+ @router.get("/", response_model=SettingsRead)
15
+ async def get_settings():
16
+ # Get singleton settings record
17
+ settings = get_singleton(Settings)
18
+ if not settings:
19
+ # This shouldn't happen because init_db creates settings
20
+ # But just in case, we'll return an error
21
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
22
+ detail="Settings record not found in database")
23
+
24
+ return settings
25
+
26
+ @router.put("/", response_model=SettingsRead)
27
+ async def update_settings(settings_update: SettingsUpdate):
28
+ try:
29
+ # Get existing settings
30
+ db_settings = get_singleton(Settings)
31
+ if not db_settings:
32
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
33
+ detail="Settings record not found in database")
34
+
35
+ # Update with new values
36
+ update_data = settings_update.model_dump(exclude_unset=True)
37
+ for key, value in update_data.items():
38
+ if value is not None: # Only update non-None values
39
+ setattr(db_settings, key, value)
40
+
41
+ # Send brightness values to FPGA via UART
42
+ red_brightness = getattr(settings_update, 'red_brightness', None)
43
+ if red_brightness is not None:
44
+ red_hex = uart.convert_brightness_to_hex(red_brightness)
45
+ red_command = uart.create_uart_message("red", red_hex)
46
+ uart.send_uart_command(red_command)
47
+
48
+ green_brightness = getattr(settings_update, 'green_brightness', None)
49
+ if green_brightness is not None:
50
+ green_hex = uart.convert_brightness_to_hex(green_brightness)
51
+ green_command = uart.create_uart_message("green", green_hex)
52
+ uart.send_uart_command(green_command)
53
+
54
+ blue_brightness = getattr(settings_update, 'blue_brightness', None)
55
+ if blue_brightness is not None:
56
+ blue_hex = uart.convert_brightness_to_hex(blue_brightness)
57
+ blue_command = uart.create_uart_message("blue", blue_hex)
58
+ uart.send_uart_command(blue_command)
59
+
60
+ # Save changes using CRUD function
61
+ db_settings = update(db_settings)
62
+
63
+ return db_settings
64
+ except Exception as e:
65
+ logging.error(f"Error updating settings: {e}")
66
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
67
+ detail=str(e))