package api import ( "database/sql" "fmt" "log" "net/http" "regexp" "strings" "pinepods/gpodder-api/internal/db" "pinepods/gpodder-api/internal/models" "github.com/gin-gonic/gin" ) // getUserLists handles GET /api/2/lists/{username}.json func getUserLists(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from middleware (if authenticated) userID, exists := c.Get("userID") username := c.Param("username") // If not authenticated, get user ID from username if !exists { var query string if database.IsPostgreSQLDB() { query = `SELECT UserID FROM "Users" WHERE Username = $1` } else { query = `SELECT UserID FROM Users WHERE Username = ?` } err := database.QueryRow(query, username).Scan(&userID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) } else { log.Printf("Error getting user ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"}) } return } } // Query for user's podcast lists var query string if database.IsPostgreSQLDB() { query = ` SELECT ListID, Name, Title FROM "GpodderSyncPodcastLists" WHERE UserID = $1 ` } else { query = ` SELECT ListID, Name, Title FROM GpodderSyncPodcastLists WHERE UserID = ? ` } rows, err := database.Query(query, userID) if err != nil { log.Printf("Error querying podcast lists: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get podcast lists"}) return } defer rows.Close() // Build response lists := make([]models.PodcastList, 0) for rows.Next() { var list models.PodcastList if err := rows.Scan(&list.ListID, &list.Name, &list.Title); err != nil { log.Printf("Error scanning podcast list: %v", err) continue } // Generate web URL list.WebURL = fmt.Sprintf("/user/%s/lists/%s", username, list.Name) lists = append(lists, list) } c.JSON(http.StatusOK, lists) } } // createPodcastList handles POST /api/2/lists/{username}/create func createPodcastList(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from middleware userID, _ := c.Get("userID") username := c.Param("username") // Get title from query parameter title := c.Query("title") if title == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Title is required"}) return } // Get format from query parameter or default to json format := c.Query("format") if format == "" { format = "json" } // Parse body for podcast URLs var podcastURLs []string switch format { case "json": if err := c.ShouldBindJSON(&podcastURLs); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } case "txt": body, err := c.GetRawData() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) return } // Split by newlines lines := strings.Split(string(body), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line != "" { podcastURLs = append(podcastURLs, line) } } default: c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"}) return } // Generate name from title name := generateNameFromTitle(title) // 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() } }() // Check if a list with this name already exists var existingID int var existsQuery string if database.IsPostgreSQLDB() { existsQuery = ` SELECT ListID FROM "GpodderSyncPodcastLists" WHERE UserID = $1 AND Name = $2 ` } else { existsQuery = ` SELECT ListID FROM GpodderSyncPodcastLists WHERE UserID = ? AND Name = ? ` } err = tx.QueryRow(existsQuery, userID, name).Scan(&existingID) if err == nil { // List already exists c.JSON(http.StatusConflict, gin.H{"error": "A podcast list with this name already exists"}) return } else if err != sql.ErrNoRows { log.Printf("Error checking list existence: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check list existence"}) return } // Create new list var listID int if database.IsPostgreSQLDB() { err = tx.QueryRow(` INSERT INTO "GpodderSyncPodcastLists" (UserID, Name, Title) VALUES ($1, $2, $3) RETURNING ListID `, userID, name, title).Scan(&listID) } else { var result sql.Result result, err = tx.Exec(` INSERT INTO GpodderSyncPodcastLists (UserID, Name, Title) VALUES (?, ?, ?) `, userID, name, title) if err != nil { log.Printf("Error creating podcast list: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create podcast list"}) 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 create podcast list"}) return } listID = int(lastID) } if err != nil { log.Printf("Error creating podcast list: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create podcast list"}) return } // Add podcasts to list for _, url := range podcastURLs { var insertQuery string if database.IsPostgreSQLDB() { insertQuery = ` INSERT INTO "GpodderSyncPodcastListEntries" (ListID, PodcastURL) VALUES ($1, $2) ` } else { insertQuery = ` INSERT INTO GpodderSyncPodcastListEntries (ListID, PodcastURL) VALUES (?, ?) ` } _, err = tx.Exec(insertQuery, listID, url) if err != nil { log.Printf("Error adding podcast to list: %v", err) // Continue with other podcasts } } // 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 success with redirect location c.Header("Location", fmt.Sprintf("/api/2/lists/%s/list/%s?format=%s", username, name, format)) c.Status(http.StatusSeeOther) } } // getPodcastList handles GET /api/2/lists/{username}/list/{listname} func getPodcastList(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get username and listname from URL username := c.Param("username") listName := c.Param("listname") // Get format from query parameter or default to json format := c.Query("format") if format == "" { format = "json" } // Get user ID from username var userID int var query string if database.IsPostgreSQLDB() { query = `SELECT UserID FROM "Users" WHERE Username = $1` } else { query = `SELECT UserID FROM Users WHERE Username = ?` } err := database.QueryRow(query, username).Scan(&userID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) } else { log.Printf("Error getting user ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"}) } return } // Get list info var listID int var title string if database.IsPostgreSQLDB() { query = ` SELECT ListID, Title FROM "GpodderSyncPodcastLists" WHERE UserID = $1 AND Name = $2 ` } else { query = ` SELECT ListID, Title FROM GpodderSyncPodcastLists WHERE UserID = ? AND Name = ? ` } err = database.QueryRow(query, userID, listName).Scan(&listID, &title) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Podcast list not found"}) } else { log.Printf("Error getting podcast list: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get podcast list"}) } return } // Get podcasts in list var rows *sql.Rows if database.IsPostgreSQLDB() { query = ` SELECT e.PodcastURL, p.PodcastName, p.Description, p.Author, p.ArtworkURL, p.WebsiteURL FROM "GpodderSyncPodcastListEntries" e LEFT JOIN "Podcasts" p ON e.PodcastURL = p.FeedURL WHERE e.ListID = $1 ` rows, err = database.Query(query, listID) } else { query = ` SELECT e.PodcastURL, p.PodcastName, p.Description, p.Author, p.ArtworkURL, p.WebsiteURL FROM GpodderSyncPodcastListEntries e LEFT JOIN Podcasts p ON e.PodcastURL = p.FeedURL WHERE e.ListID = ? ` rows, err = database.Query(query, listID) } if err != nil { log.Printf("Error querying podcasts in list: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get podcasts in list"}) return } defer rows.Close() // Build podcast list podcasts := make([]models.Podcast, 0) for rows.Next() { var podcast models.Podcast var podcastName, description, author, artworkURL, websiteURL sql.NullString if err := rows.Scan(&podcast.URL, &podcastName, &description, &author, &artworkURL, &websiteURL); err != nil { log.Printf("Error scanning podcast: %v", err) continue } // Set values if present if podcastName.Valid { podcast.Title = podcastName.String } else { podcast.Title = podcast.URL } 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 } // Add MygpoLink podcast.MygpoLink = fmt.Sprintf("/podcast/%s", podcast.URL) podcasts = append(podcasts, podcast) } // Return in requested format switch format { case "json": c.JSON(http.StatusOK, podcasts) case "txt": // Plain text format - just URLs var sb strings.Builder for _, podcast := range podcasts { sb.WriteString(podcast.URL) sb.WriteString("\n") } c.String(http.StatusOK, sb.String()) default: c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"}) } } } // updatePodcastList handles PUT /api/2/lists/{username}/list/{listname} // updatePodcastList handles PUT /api/2/lists/{username}/list/{listname} func updatePodcastList(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from middleware userID, _ := c.Get("userID") listName := c.Param("listname") // Get format from query parameter or default to json format := c.Query("format") if format == "" { format = "json" } // Parse body for podcast URLs var podcastURLs []string switch format { case "json": if err := c.ShouldBindJSON(&podcastURLs); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } case "txt": body, err := c.GetRawData() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) return } // Split by newlines lines := strings.Split(string(body), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line != "" { podcastURLs = append(podcastURLs, line) } } default: c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"}) 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 list ID var listID int var query string if database.IsPostgreSQLDB() { query = ` SELECT ListID FROM "GpodderSyncPodcastLists" WHERE UserID = $1 AND Name = $2 ` } else { query = ` SELECT ListID FROM GpodderSyncPodcastLists WHERE UserID = ? AND Name = ? ` } err = tx.QueryRow(query, userID, listName).Scan(&listID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Podcast list not found"}) } else { log.Printf("Error getting podcast list: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get podcast list"}) } return } // Remove existing entries if database.IsPostgreSQLDB() { query = ` DELETE FROM "GpodderSyncPodcastListEntries" WHERE ListID = $1 ` } else { query = ` DELETE FROM GpodderSyncPodcastListEntries WHERE ListID = ? ` } _, err = tx.Exec(query, listID) if err != nil { log.Printf("Error removing existing entries: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update podcast list"}) return } // Add new entries for _, url := range podcastURLs { if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderSyncPodcastListEntries" (ListID, PodcastURL) VALUES ($1, $2) ` } else { query = ` INSERT INTO GpodderSyncPodcastListEntries (ListID, PodcastURL) VALUES (?, ?) ` } _, err = tx.Exec(query, listID, url) if err != nil { log.Printf("Error adding podcast to list: %v", err) // Continue with other podcasts } } // 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 success c.Status(http.StatusNoContent) } } // deletePodcastList handles DELETE /api/2/lists/{username}/list/{listname} // deletePodcastList handles DELETE /api/2/lists/{username}/list/{listname} func deletePodcastList(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from middleware userID, _ := c.Get("userID") listName := c.Param("listname") // 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 list ID var listID int var query string if database.IsPostgreSQLDB() { query = ` SELECT ListID FROM "GpodderSyncPodcastLists" WHERE UserID = $1 AND Name = $2 ` } else { query = ` SELECT ListID FROM GpodderSyncPodcastLists WHERE UserID = ? AND Name = ? ` } err = tx.QueryRow(query, userID, listName).Scan(&listID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Podcast list not found"}) } else { log.Printf("Error getting podcast list: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get podcast list"}) } return } // Delete list entries first (cascade should handle this, but being explicit) if database.IsPostgreSQLDB() { query = ` DELETE FROM "GpodderSyncPodcastListEntries" WHERE ListID = $1 ` } else { query = ` DELETE FROM GpodderSyncPodcastListEntries WHERE ListID = ? ` } _, err = tx.Exec(query, listID) if err != nil { log.Printf("Error deleting list entries: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete podcast list"}) return } // Delete list if database.IsPostgreSQLDB() { query = ` DELETE FROM "GpodderSyncPodcastLists" WHERE ListID = $1 ` } else { query = ` DELETE FROM GpodderSyncPodcastLists WHERE ListID = ? ` } _, err = tx.Exec(query, listID) if err != nil { log.Printf("Error deleting podcast list: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete podcast list"}) return } // 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 success c.Status(http.StatusNoContent) } } // Helper function to generate a URL-friendly name from a title func generateNameFromTitle(title string) string { // Convert to lowercase name := strings.ToLower(title) // Replace spaces with hyphens name = strings.ReplaceAll(name, " ", "-") // Remove special characters re := regexp.MustCompile(`[^a-z0-9-]`) name = re.ReplaceAllString(name, "") // Ensure name is not empty if name == "" { name = "list" } return name }