package api import ( "database/sql" "fmt" "log" "net/http" "strings" "time" "pinepods/gpodder-api/internal/db" "pinepods/gpodder-api/internal/models" "github.com/gin-gonic/gin" ) // ValidDeviceTypes contains the allowed device types according to the gpodder API var ValidDeviceTypes = map[string]bool{ "desktop": true, "laptop": true, "mobile": true, "server": true, "other": true, } func listDevices(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { log.Printf("[DEBUG] listDevices handling request: %s %s", c.Request.Method, c.Request.URL.Path) // Log headers for debugging headers := c.Request.Header for name, values := range headers { for _, value := range values { log.Printf("[DEBUG] Header: %s: %s", name, value) } } // Log cookies cookies := c.Request.Cookies() for _, cookie := range cookies { log.Printf("[DEBUG] Cookie: %s: %s", cookie.Name, cookie.Value) } // Get user ID from context userID, exists := c.Get("userID") if !exists { log.Printf("[ERROR] listDevices: userID not found in context") c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Fix for the listDevices function // Replace the query code in listDevices function with this: log.Printf("[DEBUG] listDevices: Querying devices for userID: %v", userID) var query string var rows *sql.Rows var err error // Format query according to database type if database.IsPostgreSQLDB() { query = ` SELECT d.DeviceID, d.DeviceName, d.DeviceType, COALESCE(d.DeviceCaption, '') as DeviceCaption, d.IsActive, COALESCE( (SELECT COUNT(p.PodcastID) FROM "Podcasts" p WHERE p.UserID = $1), 0 ) as subscription_count FROM "GpodderDevices" d WHERE d.UserID = $1 AND d.IsActive = true ` rows, err = database.Query(query, userID) } else { query = ` SELECT d.DeviceID, d.DeviceName, d.DeviceType, COALESCE(d.DeviceCaption, '') as DeviceCaption, d.IsActive, COALESCE( (SELECT COUNT(p.PodcastID) FROM Podcasts p WHERE p.UserID = ?), 0 ) as subscription_count FROM GpodderDevices d WHERE d.UserID = ? AND d.IsActive = true ` rows, err = database.Query(query, userID, userID) // Note: passing userID twice for MySQL } if err != nil { log.Printf("[ERROR] listDevices: Error querying devices: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get devices"}) return } defer rows.Close() var devices []models.GpodderDevice for rows.Next() { var device models.GpodderDevice var isActive bool if err := rows.Scan( &device.DeviceID, &device.DeviceName, &device.DeviceType, &device.DeviceCaption, &isActive, &device.Subscriptions, ); err != nil { log.Printf("[ERROR] listDevices: Error scanning device row: %v", err) continue // Continue instead of returning to try to get at least some devices } // Only add active devices if isActive { log.Printf("[DEBUG] listDevices: Found active device: %s (ID: %d)", device.DeviceName, device.DeviceID) devices = append(devices, device) } } if err := rows.Err(); err != nil { log.Printf("[ERROR] listDevices: Error iterating device rows: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get devices"}) return } // If no devices found, return empty array rather than error if len(devices) == 0 { log.Printf("[DEBUG] listDevices: No devices found for userID: %v", userID) c.JSON(http.StatusOK, []models.GpodderDevice{}) return } log.Printf("[DEBUG] listDevices: Returning %d devices for userID: %v", len(devices), userID) // Return the list of devices c.JSON(http.StatusOK, devices) } } // updateDeviceData handles POST /api/2/devices/{username}/{deviceid}.json func updateDeviceData(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { log.Printf("[DEBUG] updateDeviceData handling request: %s %s", c.Request.Method, c.Request.URL.Path) // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { log.Printf("[ERROR] updateDeviceData: userID not found in context") c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Get device name from URL with fix for .json suffix deviceName := c.Param("deviceid") // Also try alternative parameter name if needed if deviceName == "" { deviceName = c.Param("deviceid.json") } // Remove .json suffix if present if strings.HasSuffix(deviceName, ".json") { deviceName = strings.TrimSuffix(deviceName, ".json") } log.Printf("[DEBUG] updateDeviceData: Using device name: '%s'", deviceName) if deviceName == "" { log.Printf("[ERROR] updateDeviceData: Device ID is required") c.JSON(http.StatusBadRequest, gin.H{"error": "Device ID is required"}) return } // Parse request body var req struct { Caption string `json:"caption"` Type string `json:"type"` } if err := c.ShouldBindJSON(&req); err != nil { log.Printf("[ERROR] updateDeviceData: Error parsing request body: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: expected a JSON object with 'caption' and 'type'"}) return } log.Printf("[DEBUG] updateDeviceData: Device info - Name: %s, Caption: %s, Type: %s", deviceName, req.Caption, req.Type) // Validate device type if provided if req.Type != "" && !ValidDeviceTypes[req.Type] { log.Printf("[ERROR] updateDeviceData: Invalid device type: %s", req.Type) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Invalid device type: %s. Valid types are: desktop, laptop, mobile, server, other", req.Type), }) return } // If type is empty, set to default 'other' if req.Type == "" { req.Type = "other" } // Begin transaction tx, err := database.Begin() if err != nil { log.Printf("[ERROR] updateDeviceData: Error beginning transaction: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"}) return } defer func() { if err != nil { tx.Rollback() } }() // Check if device exists var deviceID int var query string log.Printf("[DEBUG] updateDeviceData: Checking if device exists - UserID: %v, DeviceName: %s", userID, deviceName) if database.IsPostgreSQLDB() { query = `SELECT DeviceID FROM "GpodderDevices" WHERE UserID = $1 AND DeviceName = $2` } else { query = `SELECT DeviceID FROM GpodderDevices WHERE UserID = ? AND DeviceName = ?` } err = tx.QueryRow(query, userID, deviceName).Scan(&deviceID) if err != nil { if err == sql.ErrNoRows { // Device doesn't exist, create it log.Printf("[DEBUG] updateDeviceData: Creating new device - UserID: %v, DeviceName: %s, Type: %s", userID, deviceName, req.Type) if database.IsPostgreSQLDB() { query = `INSERT INTO "GpodderDevices" (UserID, DeviceName, DeviceType, DeviceCaption, IsActive, LastSync) VALUES ($1, $2, $3, $4, true, $5) RETURNING DeviceID` err = tx.QueryRow(query, userID, deviceName, req.Type, req.Caption, time.Now()).Scan(&deviceID) if err != nil { log.Printf("[ERROR] updateDeviceData: Error creating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"}) return } } else { query = `INSERT INTO GpodderDevices (UserID, DeviceName, DeviceType, DeviceCaption, IsActive, LastSync) VALUES (?, ?, ?, ?, true, ?)` result, err := tx.Exec(query, userID, deviceName, req.Type, req.Caption, time.Now()) if err != nil { log.Printf("[ERROR] updateDeviceData: Error creating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"}) return } // Get the last inserted ID lastID, err := result.LastInsertId() if err != nil { log.Printf("[ERROR] updateDeviceData: Error getting last insert ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device ID"}) return } deviceID = int(lastID) } log.Printf("[DEBUG] updateDeviceData: Created new device with ID: %d", deviceID) // Also create entry in device state table, handling both PostgreSQL and MySQL syntax if database.IsPostgreSQLDB() { query = `INSERT INTO "GpodderSyncDeviceState" (UserID, DeviceID) VALUES ($1, $2) ON CONFLICT (UserID, DeviceID) DO NOTHING` _, err = tx.Exec(query, userID, deviceID) } else { // In MySQL, use INSERT IGNORE instead of ON CONFLICT query = `INSERT IGNORE INTO GpodderSyncDeviceState (UserID, DeviceID) VALUES (?, ?)` _, err = tx.Exec(query, userID, deviceID) } // Log the device state creation result but don't fail on error if err != nil { log.Printf("[WARNING] updateDeviceData: Error creating device state: %v", err) // Not fatal, continue with response } } else { log.Printf("[ERROR] updateDeviceData: Error checking device existence: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check device"}) return } } else { // Device exists, update it log.Printf("[DEBUG] updateDeviceData: Updating existing device with ID: %d", deviceID) if database.IsPostgreSQLDB() { query = `UPDATE "GpodderDevices" SET DeviceType = $1, DeviceCaption = $2, LastSync = $3, IsActive = true WHERE DeviceID = $4` } else { query = `UPDATE GpodderDevices SET DeviceType = ?, DeviceCaption = ?, LastSync = ?, IsActive = true WHERE DeviceID = ?` } _, err = tx.Exec(query, req.Type, req.Caption, time.Now(), deviceID) if err != nil { log.Printf("[ERROR] updateDeviceData: Error updating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update device"}) return } } // Commit transaction if err = tx.Commit(); err != nil { log.Printf("[ERROR] updateDeviceData: Error committing transaction: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit changes"}) return } // Return empty response with 200 status code as per gpodder API log.Printf("[DEBUG] updateDeviceData: Successfully processed device request") c.JSON(http.StatusOK, gin.H{}) } } // getDeviceUpdates handles GET /api/2/updates/{username}/{deviceid}.json func getDeviceUpdates(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { log.Printf("[DEBUG] getDeviceUpdates: Processing request: %s %s", c.Request.Method, c.Request.URL.Path) // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Get device name from URL with fix for .json suffix deviceName := c.Param("deviceid") // Also try alternative parameter name if needed if deviceName == "" { deviceName = c.Param("deviceid.json") } // Remove .json suffix if present if strings.HasSuffix(deviceName, ".json") { deviceName = strings.TrimSuffix(deviceName, ".json") } log.Printf("[DEBUG] getDeviceUpdates: Using device name: '%s'", deviceName) // Parse query parameters sinceStr := c.Query("since") includeActions := c.Query("include_actions") == "true" var since int64 = 0 if sinceStr != "" { _, err := fmt.Sscanf(sinceStr, "%d", &since) if err != nil { log.Printf("Invalid 'since' parameter: %s - %v", sinceStr, err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'since' parameter: must be a Unix timestamp"}) return } } // Begin transaction tx, err := database.Begin() if err != nil { log.Printf("Error beginning transaction: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"}) return } defer func() { if err != nil { tx.Rollback() } }() // Get or create the device var deviceID int var query string if database.IsPostgreSQLDB() { query = ` SELECT DeviceID FROM "GpodderDevices" WHERE UserID = $1 AND DeviceName = $2 AND IsActive = true ` } else { query = ` SELECT DeviceID FROM GpodderDevices WHERE UserID = ? AND DeviceName = ? AND IsActive = true ` } err = tx.QueryRow(query, userID, deviceName).Scan(&deviceID) if err != nil { if err == sql.ErrNoRows { // Device doesn't exist or is inactive, create it log.Printf("Creating new device for updates: %s", deviceName) if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderDevices" (UserID, DeviceName, DeviceType, IsActive, LastSync) VALUES ($1, $2, 'other', true, $3) RETURNING DeviceID ` err = tx.QueryRow(query, userID, deviceName, time.Now()).Scan(&deviceID) } else { query = ` INSERT INTO GpodderDevices (UserID, DeviceName, DeviceType, IsActive, LastSync) VALUES (?, ?, 'other', true, ?) ` result, err := tx.Exec(query, userID, deviceName, time.Now()) if err != nil { log.Printf("Error creating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"}) return } lastID, err := result.LastInsertId() if err != nil { log.Printf("Error getting last insert ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device ID"}) return } deviceID = int(lastID) } if err != nil { log.Printf("Error creating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"}) return } // Also create entry in device state table if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderSyncDeviceState" (UserID, DeviceID) VALUES ($1, $2) ON CONFLICT (UserID, DeviceID) DO NOTHING ` _, err = tx.Exec(query, userID, deviceID) } else { query = ` INSERT IGNORE INTO GpodderSyncDeviceState (UserID, DeviceID) VALUES (?, ?) ` _, err = tx.Exec(query, userID, deviceID) } if err != nil { log.Printf("Error creating device state: %v", err) // Not fatal, continue } } else { log.Printf("Error getting device ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"}) return } } // Get the current timestamp for the response timestamp := time.Now().Unix() // Build the response structure response := models.DeviceUpdateResponse{ Add: []models.Podcast{}, Remove: []string{}, Updates: []models.Episode{}, Timestamp: timestamp, } // Only process updates if a since timestamp was provided if since > 0 { // Get the last sync timestamp for this device var lastSync int64 if database.IsPostgreSQLDB() { query = ` SELECT COALESCE(LastTimestamp, 0) FROM "GpodderSyncState" WHERE UserID = $1 AND DeviceID = $2 ` } else { query = ` SELECT COALESCE(LastTimestamp, 0) FROM GpodderSyncState WHERE UserID = ? AND DeviceID = ? ` } err = tx.QueryRow(query, userID, deviceID).Scan(&lastSync) if err != nil && err != sql.ErrNoRows { log.Printf("Error getting last sync timestamp: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get sync state"}) return } // Handle podcasts to add (subscribed on other devices since the timestamp) var addRows *sql.Rows if database.IsPostgreSQLDB() { query = ` SELECT DISTINCT p.FeedURL, p.PodcastName, p.Description, p.Author, p.ArtworkURL, p.WebsiteURL, (SELECT COUNT(*) FROM "Podcasts" WHERE FeedURL = p.FeedURL) as subscribers FROM "Podcasts" p JOIN "GpodderSyncSubscriptions" s ON p.FeedURL = s.PodcastURL WHERE s.UserID = $1 AND s.DeviceID != $2 AND s.Timestamp > $3 AND s.Action = 'add' AND NOT EXISTS ( SELECT 1 FROM "GpodderSyncSubscriptions" s2 WHERE s2.UserID = s.UserID AND s2.PodcastURL = s.PodcastURL AND s2.DeviceID = $2 AND s2.Timestamp > s.Timestamp AND s2.Action = 'add' ) ` addRows, err = tx.Query(query, userID, deviceID, since) } else { query = ` SELECT DISTINCT p.FeedURL, p.PodcastName, p.Description, p.Author, p.ArtworkURL, p.WebsiteURL, (SELECT COUNT(*) FROM Podcasts WHERE FeedURL = p.FeedURL) as subscribers FROM Podcasts p JOIN GpodderSyncSubscriptions s ON p.FeedURL = s.PodcastURL WHERE s.UserID = ? AND s.DeviceID != ? AND s.Timestamp > ? AND s.Action = 'add' AND NOT EXISTS ( SELECT 1 FROM GpodderSyncSubscriptions s2 WHERE s2.UserID = s.UserID AND s2.PodcastURL = s.PodcastURL AND s2.DeviceID = ? AND s2.Timestamp > s.Timestamp AND s2.Action = 'add' ) ` addRows, err = tx.Query(query, userID, deviceID, since, deviceID) // Note: deviceID is used twice in MySQL } if err != nil { log.Printf("Error getting podcasts to add: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get updates"}) return } defer addRows.Close() for addRows.Next() { var podcast models.Podcast var podcastName, description, author, artworkURL, websiteURL sql.NullString var subscribers int err := addRows.Scan( &podcast.URL, &podcastName, &description, &author, &artworkURL, &websiteURL, &subscribers, ) if err != nil { log.Printf("Error scanning podcast row: %v", err) continue } // Set title - default to URL if name is null if podcastName.Valid && podcastName.String != "" { podcast.Title = podcastName.String } else { podcast.Title = podcast.URL } // Set optional fields if present if description.Valid { podcast.Description = description.String } if author.Valid { podcast.Author = author.String } if artworkURL.Valid { podcast.LogoURL = artworkURL.String } if websiteURL.Valid { podcast.Website = websiteURL.String } podcast.Subscribers = subscribers podcast.MygpoLink = fmt.Sprintf("/podcast/%s", podcast.URL) // Add the podcast to the response response.Add = append(response.Add, podcast) } if err = addRows.Err(); err != nil { log.Printf("Error iterating add rows: %v", err) // Continue processing other updates } // Query podcasts to remove (unsubscribed on other devices) var removeRows *sql.Rows if database.IsPostgreSQLDB() { query = ` SELECT DISTINCT s.PodcastURL FROM "GpodderSyncSubscriptions" s WHERE s.UserID = $1 AND s.DeviceID != $2 AND s.Timestamp > $3 AND s.Action = 'remove' AND NOT EXISTS ( SELECT 1 FROM "GpodderSyncSubscriptions" s2 WHERE s2.UserID = s.UserID AND s2.PodcastURL = s.PodcastURL AND s2.DeviceID = $2 AND s2.Timestamp > s.Timestamp AND s2.Action = 'add' ) ` removeRows, err = tx.Query(query, userID, deviceID, since) } else { query = ` SELECT DISTINCT s.PodcastURL FROM GpodderSyncSubscriptions s WHERE s.UserID = ? AND s.DeviceID != ? AND s.Timestamp > ? AND s.Action = 'remove' AND NOT EXISTS ( SELECT 1 FROM GpodderSyncSubscriptions s2 WHERE s2.UserID = s.UserID AND s2.PodcastURL = s.PodcastURL AND s2.DeviceID = ? AND s2.Timestamp > s.Timestamp AND s2.Action = 'add' ) ` removeRows, err = tx.Query(query, userID, deviceID, since, deviceID) // Note: deviceID is used twice } if err != nil { log.Printf("Error getting podcasts to remove: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get updates"}) return } defer removeRows.Close() for removeRows.Next() { var podcastURL string err := removeRows.Scan(&podcastURL) if err != nil { log.Printf("Error scanning podcast URL: %v", err) continue } // Add the podcast URL to the response response.Remove = append(response.Remove, podcastURL) } if err = removeRows.Err(); err != nil { log.Printf("Error iterating remove rows: %v", err) // Continue processing other updates } // Query episode updates (if includeActions is true) if includeActions { var updateRows *sql.Rows if database.IsPostgreSQLDB() { query = ` SELECT e.EpisodeTitle, e.EpisodeURL, p.PodcastName, p.FeedURL, e.EpisodeDescription, e.EpisodeURL, e.EpisodePubDate, a.Action, a.Position, a.Total, a.Started FROM "GpodderSyncEpisodeActions" a JOIN "Episodes" e ON a.EpisodeURL = e.EpisodeURL JOIN "Podcasts" p ON e.PodcastID = p.PodcastID WHERE a.UserID = $1 AND a.Timestamp > $2 AND a.Action != 'new' ORDER BY a.Timestamp DESC ` updateRows, err = tx.Query(query, userID, since) } else { query = ` SELECT e.EpisodeTitle, e.EpisodeURL, p.PodcastName, p.FeedURL, e.EpisodeDescription, e.EpisodeURL, e.EpisodePubDate, a.Action, a.Position, a.Total, a.Started FROM GpodderSyncEpisodeActions a JOIN Episodes e ON a.EpisodeURL = e.EpisodeURL JOIN Podcasts p ON e.PodcastID = p.PodcastID WHERE a.UserID = ? AND a.Timestamp > ? AND a.Action != 'new' ORDER BY a.Timestamp DESC ` updateRows, err = tx.Query(query, userID, since) } if err != nil { log.Printf("Error getting episode updates: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get episode updates"}) return } defer updateRows.Close() for updateRows.Next() { var episode models.Episode var pubDate time.Time var action string var position, total, started sql.NullInt64 err := updateRows.Scan( &episode.Title, &episode.URL, &episode.PodcastTitle, &episode.PodcastURL, &episode.Description, &episode.Website, &pubDate, &action, &position, &total, &started, ) if err != nil { log.Printf("Error scanning episode row: %v", err) continue } // Format the publication date in ISO 8601 format episode.Released = pubDate.Format(time.RFC3339) // Add the episode to the response response.Updates = append(response.Updates, episode) } if err = updateRows.Err(); err != nil { log.Printf("Error iterating episode update rows: %v", err) // Continue with other processing } } } // Update the last sync timestamp for this device if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderSyncState" (UserID, DeviceID, LastTimestamp) VALUES ($1, $2, $3) ON CONFLICT (UserID, DeviceID) DO UPDATE SET LastTimestamp = $3 ` _, err = tx.Exec(query, userID, deviceID, timestamp) } else { // MySQL uses INSERT ... ON DUPLICATE KEY UPDATE syntax query = ` INSERT INTO GpodderSyncState (UserID, DeviceID, LastTimestamp) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE LastTimestamp = ? ` _, err = tx.Exec(query, userID, deviceID, timestamp, timestamp) } if err != nil { log.Printf("Error updating device sync state: %v", err) // Not fatal, continue with response } // Update the device LastSync if database.IsPostgreSQLDB() { query = ` UPDATE "GpodderDevices" SET LastSync = $1 WHERE DeviceID = $2 ` _, err = tx.Exec(query, time.Now(), deviceID) } else { query = ` UPDATE GpodderDevices SET LastSync = ? WHERE DeviceID = ? ` _, err = tx.Exec(query, time.Now(), deviceID) } if err != nil { log.Printf("Error updating device last sync time: %v", err) // Non-critical error, continue } // Commit transaction if err = tx.Commit(); err != nil { log.Printf("Error committing transaction: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit changes"}) return } // Return the response c.JSON(http.StatusOK, response) } } // deactivateDevice handles DELETE /api/2/devices/{username}/{deviceid}.json // This is an extension to the gpodder API for device management func deactivateDevice(database *db.PostgresDB) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Get device name from URL // Get device name from URL with fix for .json suffix deviceName := c.Param("deviceid") // Also try alternative parameter name if needed if deviceName == "" { deviceName = c.Param("deviceid.json") } // Remove .json suffix if present if strings.HasSuffix(deviceName, ".json") { deviceName = strings.TrimSuffix(deviceName, ".json") } log.Printf("[DEBUG] deactivateDevice: Using device name: '%s'", deviceName) // Get the device ID var deviceID int err := database.QueryRow(` SELECT DeviceID FROM "GpodderDevices" WHERE UserID = $1 AND DeviceName = $2 `, userID, deviceName).Scan(&deviceID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"}) } else { log.Printf("Error getting device ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"}) } return } // Deactivate the device (rather than delete, to preserve history) _, err = database.Exec(` UPDATE "GpodderDevices" SET IsActive = false WHERE DeviceID = $1 `, deviceID) if err != nil { log.Printf("Error deactivating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to deactivate device"}) return } // Return success c.JSON(http.StatusOK, gin.H{ "result": "success", "message": "Device deactivated", }) } } // renameDevice handles PUT /api/2/devices/{username}/{deviceid}/rename.json // This is an extension to the gpodder API for device management func renameDevice(database *db.PostgresDB) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Get device name from URL oldDeviceName := c.Param("deviceid") if oldDeviceName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Device ID is required"}) return } // Parse request body var req struct { NewDeviceName string `json:"new_deviceid"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: expected a JSON object with 'new_deviceid'"}) return } if req.NewDeviceName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "New device ID is required"}) return } // Check if the new device name already exists var existingCount int err := database.QueryRow(` SELECT COUNT(*) FROM "GpodderDevices" WHERE UserID = $1 AND DeviceName = $2 AND IsActive = true `, userID, req.NewDeviceName).Scan(&existingCount) if err != nil { log.Printf("Error checking for existing device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check for existing device"}) return } if existingCount > 0 { c.JSON(http.StatusConflict, gin.H{"error": "Device with this name already exists"}) return } // Update the device name result, err := database.Exec(` UPDATE "GpodderDevices" SET DeviceName = $1, LastSync = $2 WHERE UserID = $3 AND DeviceName = $4 AND IsActive = true `, req.NewDeviceName, time.Now(), userID, oldDeviceName) if err != nil { log.Printf("Error renaming device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to rename device"}) return } rowsAffected, err := result.RowsAffected() if err != nil { log.Printf("Error getting rows affected: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get operation result"}) return } if rowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "Device not found or not active"}) return } // Return success c.JSON(http.StatusOK, gin.H{ "result": "success", "message": "Device renamed successfully", }) } } // deviceSync represents the synchronization state of a device type deviceSync struct { LastSync time.Time `json:"last_sync"` DeviceID int `json:"-"` DeviceName string `json:"device_id"` DeviceType string `json:"device_type"` IsActive bool `json:"-"` SyncEnabled bool `json:"sync_enabled"` } // getDeviceSyncStatus handles GET /api/2/devices/{username}/sync.json // This is an extension to the gpodder API for device sync status func getDeviceSyncStatus(database *db.PostgresDB) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Query all devices and their sync status rows, err := database.Query(` SELECT d.DeviceID, d.DeviceName, d.DeviceType, d.LastSync, d.IsActive, EXISTS ( SELECT 1 FROM "GpodderSyncDevicePairs" p WHERE (p.DeviceID1 = d.DeviceID OR p.DeviceID2 = d.DeviceID) AND p.UserID = d.UserID ) as sync_enabled FROM "GpodderDevices" d WHERE d.UserID = $1 ORDER BY d.LastSync DESC `, userID) if err != nil { log.Printf("Error querying device sync status: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device sync status"}) return } defer rows.Close() devices := make([]deviceSync, 0) for rows.Next() { var device deviceSync var lastSync sql.NullTime if err := rows.Scan( &device.DeviceID, &device.DeviceName, &device.DeviceType, &lastSync, &device.IsActive, &device.SyncEnabled, ); err != nil { log.Printf("Error scanning device sync row: %v", err) continue } // Set the last sync time if valid if lastSync.Valid { device.LastSync = lastSync.Time } // Only include active devices if device.IsActive { devices = append(devices, device) } } if err = rows.Err(); err != nil { log.Printf("Error iterating device sync rows: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process device sync status"}) return } // Return the response c.JSON(http.StatusOK, gin.H{ "devices": devices, "timestamp": time.Now().Unix(), }) } }