Files
PinePods-nix/PinePods-0.8.2/gpodder-api/internal/api/episode.go
2026-03-03 10:57:43 -05:00

845 lines
26 KiB
Go

package api
import (
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"pinepods/gpodder-api/internal/db"
"pinepods/gpodder-api/internal/models"
"github.com/gin-gonic/gin"
)
// getEpisodeActions handles GET /api/2/episodes/{username}.json
func getEpisodeActions(database *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from middleware
userID, exists := c.Get("userID")
if !exists {
log.Printf("[ERROR] getEpisodeActions: userID not found in context")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Parse query parameters
sinceStr := c.Query("since")
podcastURL := c.Query("podcast")
deviceName := c.Query("device")
aggregated := c.Query("aggregated") == "true"
// Get device ID if provided
var deviceID *int
if deviceName != "" {
var deviceIDInt 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 := database.QueryRow(query, userID, deviceName).Scan(&deviceIDInt)
if err != nil {
if err != sql.ErrNoRows {
log.Printf("[ERROR] getEpisodeActions: Error getting device: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"})
return
}
// Device not found is not fatal if querying by device
} else {
deviceID = &deviceIDInt
}
}
var since int64 = 0
if sinceStr != "" {
var err error
since, err = strconv.ParseInt(sinceStr, 10, 64)
if err != nil {
log.Printf("[ERROR] getEpisodeActions: Invalid since parameter: %s", sinceStr)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid since parameter: must be a Unix timestamp"})
return
}
}
// Get the latest timestamp for the response
var latestTimestamp int64
var timestampQuery string
if database.IsPostgreSQLDB() {
timestampQuery = `
SELECT COALESCE(MAX(Timestamp), EXTRACT(EPOCH FROM NOW())::bigint)
FROM "GpodderSyncEpisodeActions"
WHERE UserID = $1
`
} else {
timestampQuery = `
SELECT COALESCE(MAX(Timestamp), UNIX_TIMESTAMP())
FROM GpodderSyncEpisodeActions
WHERE UserID = ?
`
}
err := database.QueryRow(timestampQuery, userID).Scan(&latestTimestamp)
if err != nil {
log.Printf("[ERROR] getEpisodeActions: Error getting latest timestamp: %v", err)
latestTimestamp = time.Now().Unix() // Fallback to current time
}
// Performance optimization: Add limits and optimize query structure
const MAX_EPISODE_ACTIONS = 10000 // Reasonable limit for sync operations
// Log query performance info
log.Printf("[DEBUG] getEpisodeActions: Query for user %v with since=%d, device=%s, aggregated=%v",
userID, since, deviceName, aggregated)
// Build query based on parameters with performance optimizations
var queryParts []string
if database.IsPostgreSQLDB() {
queryParts = []string{
"SELECT " +
"e.ActionID, e.UserID, e.DeviceID, e.PodcastURL, e.EpisodeURL, " +
"e.Action, e.Timestamp, e.Started, e.Position, e.Total, " +
"COALESCE(d.DeviceName, '') as DeviceName " +
"FROM \"GpodderSyncEpisodeActions\" e " +
"LEFT JOIN \"GpodderDevices\" d ON e.DeviceID = d.DeviceID " +
"WHERE e.UserID = $1",
}
} else {
queryParts = []string{
"SELECT " +
"e.ActionID, e.UserID, e.DeviceID, e.PodcastURL, e.EpisodeURL, " +
"e.Action, e.Timestamp, e.Started, e.Position, e.Total, " +
"COALESCE(d.DeviceName, '') as DeviceName " +
"FROM GpodderSyncEpisodeActions e " +
"LEFT JOIN GpodderDevices d ON e.DeviceID = d.DeviceID " +
"WHERE e.UserID = ?",
}
}
args := []interface{}{userID}
paramCount := 2
// For aggregated results, we need a more complex query
var query string
if aggregated {
if database.IsPostgreSQLDB() {
// Build conditions for the subquery
var conditions []string
if since > 0 {
conditions = append(conditions, fmt.Sprintf("AND e.Timestamp > $%d", paramCount))
args = append(args, since)
paramCount++
}
if podcastURL != "" {
conditions = append(conditions, fmt.Sprintf("AND e.PodcastURL = $%d", paramCount))
args = append(args, podcastURL)
paramCount++
}
if deviceID != nil {
conditions = append(conditions, fmt.Sprintf("AND e.DeviceID = $%d", paramCount))
args = append(args, *deviceID)
paramCount++
}
conditionsStr := strings.Join(conditions, " ")
query = fmt.Sprintf(`
WITH latest_actions AS (
SELECT
e.PodcastURL,
e.EpisodeURL,
MAX(e.Timestamp) as max_timestamp
FROM "GpodderSyncEpisodeActions" e
WHERE e.UserID = $1
%s
GROUP BY e.PodcastURL, e.EpisodeURL
)
SELECT
e.ActionID, e.UserID, e.DeviceID, e.PodcastURL, e.EpisodeURL,
e.Action, e.Timestamp, e.Started, e.Position, e.Total,
d.DeviceName
FROM "GpodderSyncEpisodeActions" e
JOIN latest_actions la ON
e.PodcastURL = la.PodcastURL AND
e.EpisodeURL = la.EpisodeURL AND
e.Timestamp = la.max_timestamp
LEFT JOIN "GpodderDevices" d ON e.DeviceID = d.DeviceID
WHERE e.UserID = $1
ORDER BY e.Timestamp DESC
LIMIT %d
`, conditionsStr, MAX_EPISODE_ACTIONS)
} else {
// For MySQL, we need to use ? placeholders and rebuild the argument list
args = []interface{}{userID} // Reset args to just include userID for now
// Build conditions for the subquery
var conditions []string
if since > 0 {
conditions = append(conditions, "AND e.Timestamp > ?")
args = append(args, since)
}
if podcastURL != "" {
conditions = append(conditions, "AND e.PodcastURL = ?")
args = append(args, podcastURL)
}
if deviceID != nil {
conditions = append(conditions, "AND e.DeviceID = ?")
args = append(args, *deviceID)
}
conditionsStr := strings.Join(conditions, " ")
// Need to duplicate userID in args for the second part of the query
mysqlArgs := make([]interface{}, len(args))
copy(mysqlArgs, args)
args = append(args, mysqlArgs...)
query = fmt.Sprintf(`
WITH latest_actions AS (
SELECT
e.PodcastURL,
e.EpisodeURL,
MAX(e.Timestamp) as max_timestamp
FROM GpodderSyncEpisodeActions e
WHERE e.UserID = ?
%s
GROUP BY e.PodcastURL, e.EpisodeURL
)
SELECT
e.ActionID, e.UserID, e.DeviceID, e.PodcastURL, e.EpisodeURL,
e.Action, e.Timestamp, e.Started, e.Position, e.Total,
d.DeviceName
FROM GpodderSyncEpisodeActions e
JOIN latest_actions la ON
e.PodcastURL = la.PodcastURL AND
e.EpisodeURL = la.EpisodeURL AND
e.Timestamp = la.max_timestamp
LEFT JOIN GpodderDevices d ON e.DeviceID = d.DeviceID
WHERE e.UserID = ?
ORDER BY e.Timestamp DESC
LIMIT %d
`, conditionsStr, MAX_EPISODE_ACTIONS)
}
} else {
// Simple query with ORDER BY
if database.IsPostgreSQLDB() {
if since > 0 {
queryParts = append(queryParts, fmt.Sprintf("AND e.Timestamp > $%d", paramCount))
args = append(args, since)
paramCount++
}
if podcastURL != "" {
queryParts = append(queryParts, fmt.Sprintf("AND e.PodcastURL = $%d", paramCount))
args = append(args, podcastURL)
paramCount++
}
if deviceID != nil {
queryParts = append(queryParts, fmt.Sprintf("AND e.DeviceID = $%d", paramCount))
args = append(args, *deviceID)
paramCount++
}
} else {
if since > 0 {
queryParts = append(queryParts, "AND e.Timestamp > ?")
args = append(args, since)
}
if podcastURL != "" {
queryParts = append(queryParts, "AND e.PodcastURL = ?")
args = append(args, podcastURL)
}
if deviceID != nil {
queryParts = append(queryParts, "AND e.DeviceID = ?")
args = append(args, *deviceID)
}
}
queryParts = append(queryParts, "ORDER BY e.Timestamp DESC")
// Add LIMIT for performance - prevents returning massive datasets
if database.IsPostgreSQLDB() {
queryParts = append(queryParts, fmt.Sprintf("LIMIT %d", MAX_EPISODE_ACTIONS))
} else {
queryParts = append(queryParts, fmt.Sprintf("LIMIT %d", MAX_EPISODE_ACTIONS))
}
query = strings.Join(queryParts, " ")
}
// Execute query with timing
startTime := time.Now()
rows, err := database.Query(query, args...)
queryDuration := time.Since(startTime)
if err != nil {
log.Printf("[ERROR] getEpisodeActions: Error querying episode actions (took %v): %v", queryDuration, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get episode actions"})
return
}
defer rows.Close()
log.Printf("[DEBUG] getEpisodeActions: Query executed in %v", queryDuration)
// Build response
actions := make([]models.EpisodeAction, 0)
for rows.Next() {
var action models.EpisodeAction
var deviceIDInt sql.NullInt64
var deviceName sql.NullString
var started sql.NullInt64
var position sql.NullInt64
var total sql.NullInt64
if err := rows.Scan(
&action.ActionID,
&action.UserID,
&deviceIDInt,
&action.Podcast,
&action.Episode,
&action.Action,
&action.Timestamp,
&started,
&position,
&total,
&deviceName,
); err != nil {
log.Printf("[ERROR] getEpisodeActions: Error scanning action row: %v", err)
continue
}
// Set optional fields if present
if deviceName.Valid {
action.Device = deviceName.String
}
if started.Valid {
startedInt := int(started.Int64)
action.Started = &startedInt
}
if position.Valid {
positionInt := int(position.Int64)
action.Position = &positionInt
}
if total.Valid {
totalInt := int(total.Int64)
action.Total = &totalInt
}
actions = append(actions, action)
}
if err = rows.Err(); err != nil {
log.Printf("[ERROR] getEpisodeActions: Error iterating rows: %v", err)
// Continue with what we've got so far
}
// Log performance results
totalDuration := time.Since(startTime)
log.Printf("[DEBUG] getEpisodeActions: Returning %d actions, total time: %v", len(actions), totalDuration)
// Return response in gpodder format
c.JSON(http.StatusOK, models.EpisodeActionsResponse{
Actions: actions,
Timestamp: latestTimestamp,
})
}
}
// uploadEpisodeActions handles POST /api/2/episodes/{username}.json
func uploadEpisodeActions(database *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from middleware
userID, exists := c.Get("userID")
if !exists {
log.Printf("[ERROR] uploadEpisodeActions: userID not found in context")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Parse request - try both formats
var actions []models.EpisodeAction
// First try parsing as array directly
if err := c.ShouldBindJSON(&actions); err != nil {
// If that fails, try parsing as a wrapper object
var wrappedActions struct {
Actions []models.EpisodeAction `json:"actions"`
}
if err2 := c.ShouldBindJSON(&wrappedActions); err2 != nil {
log.Printf("[ERROR] uploadEpisodeActions: Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body format"})
return
}
actions = wrappedActions.Actions
}
// Begin transaction
tx, err := database.Begin()
if err != nil {
log.Printf("[ERROR] uploadEpisodeActions: Error beginning transaction: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"})
return
}
defer func() {
if err != nil {
tx.Rollback()
return
}
}()
// Process actions
timestamp := time.Now().Unix()
updateURLs := make([][]string, 0)
for _, action := range actions {
// Validate action
if action.Podcast == "" || action.Episode == "" || action.Action == "" {
log.Printf("[WARNING] uploadEpisodeActions: Skipping invalid action: podcast=%s, episode=%s, action=%s",
action.Podcast, action.Episode, action.Action)
continue
}
// Clean URLs if needed
cleanPodcastURL, err := sanitizeURL(action.Podcast)
if err != nil {
log.Printf("[WARNING] uploadEpisodeActions: Error sanitizing podcast URL %s: %v", action.Podcast, err)
cleanPodcastURL = action.Podcast // Use original if sanitization fails
}
cleanEpisodeURL, err := sanitizeURL(action.Episode)
if err != nil {
log.Printf("[WARNING] uploadEpisodeActions: Error sanitizing episode URL %s: %v", action.Episode, err)
cleanEpisodeURL = action.Episode // Use original if sanitization fails
}
// Get or create device ID if provided
var deviceID sql.NullInt64
if action.Device != "" {
var query string
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, action.Device).Scan(&deviceID.Int64)
if err != nil {
if err == sql.ErrNoRows {
// Create the device if it doesn't exist
if database.IsPostgreSQLDB() {
query = `
INSERT INTO "GpodderDevices" (UserID, DeviceName, DeviceType, IsActive, LastSync)
VALUES ($1, $2, 'other', true, CURRENT_TIMESTAMP)
RETURNING DeviceID
`
err = tx.QueryRow(query, userID, action.Device).Scan(&deviceID.Int64)
} else {
query = `
INSERT INTO GpodderDevices (UserID, DeviceName, DeviceType, IsActive, LastSync)
VALUES (?, ?, 'other', true, CURRENT_TIMESTAMP)
`
result, err := tx.Exec(query, userID, action.Device, "other")
if err != nil {
log.Printf("[ERROR] uploadEpisodeActions: 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] uploadEpisodeActions: Error getting last insert ID: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"})
return
}
deviceID.Int64 = lastID
}
if err != nil {
log.Printf("[ERROR] uploadEpisodeActions: Error creating device: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"})
return
}
deviceID.Valid = true
} else {
log.Printf("[ERROR] uploadEpisodeActions: Error getting device ID: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"})
return
}
} else {
deviceID.Valid = true
}
}
// Parse timestamp from interface{} to int64
actionTimestamp := timestamp
if action.Timestamp != nil {
switch t := action.Timestamp.(type) {
case float64:
actionTimestamp = int64(t)
case int64:
actionTimestamp = t
case int:
actionTimestamp = int64(t)
case string:
// First try to parse as Unix timestamp
if ts, err := strconv.ParseInt(t, 10, 64); err == nil {
actionTimestamp = ts
} else {
// Try parsing as ISO date (2025-04-23T12:18:51)
if parsedTime, err := time.Parse(time.RFC3339, t); err == nil {
actionTimestamp = parsedTime.Unix()
log.Printf("[DEBUG] uploadEpisodeActions: Parsed ISO timestamp '%s' to Unix timestamp %d", t, actionTimestamp)
} else {
// Try some other common formats
formats := []string{
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05",
"2006-01-02",
}
parsed := false
for _, format := range formats {
if parsedTime, err := time.Parse(format, t); err == nil {
actionTimestamp = parsedTime.Unix()
parsed = true
break
}
}
if !parsed {
log.Printf("[WARNING] uploadEpisodeActions: Could not parse timestamp '%s', using current time", t)
}
}
}
default:
log.Printf("[WARNING] uploadEpisodeActions: Unknown timestamp type, using current time")
}
}
// Insert action
var insertQuery string
if database.IsPostgreSQLDB() {
insertQuery = `
INSERT INTO "GpodderSyncEpisodeActions"
(UserID, DeviceID, PodcastURL, EpisodeURL, Action, Timestamp, Started, Position, Total)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
} else {
insertQuery = `
INSERT INTO GpodderSyncEpisodeActions
(UserID, DeviceID, PodcastURL, EpisodeURL, Action, Timestamp, Started, Position, Total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`
}
_, err = tx.Exec(insertQuery,
userID,
deviceID,
cleanPodcastURL,
cleanEpisodeURL,
action.Action,
actionTimestamp,
action.Started,
action.Position,
action.Total)
if err != nil {
log.Printf("[ERROR] uploadEpisodeActions: Error inserting episode action: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save episode action"})
return
}
// Add to updateURLs if URLs were cleaned
if cleanPodcastURL != action.Podcast {
updateURLs = append(updateURLs, []string{action.Podcast, cleanPodcastURL})
}
if cleanEpisodeURL != action.Episode {
updateURLs = append(updateURLs, []string{action.Episode, cleanEpisodeURL})
}
// For play action with position > 0, update episode status in Pinepods database
if action.Action == "play" && action.Position != nil && *action.Position > 0 {
// Try to find episode ID in Episodes table
var episodeID int
var findEpisodeQuery string
if database.IsPostgreSQLDB() {
findEpisodeQuery = `
SELECT e.EpisodeID
FROM "Episodes" e
JOIN "Podcasts" p ON e.PodcastID = p.PodcastID
WHERE p.FeedURL = $1 AND e.EpisodeURL = $2 AND p.UserID = $3
`
} else {
findEpisodeQuery = `
SELECT e.EpisodeID
FROM Episodes e
JOIN Podcasts p ON e.PodcastID = p.PodcastID
WHERE p.FeedURL = ? AND e.EpisodeURL = ? AND p.UserID = ?
`
}
err := tx.QueryRow(findEpisodeQuery, cleanPodcastURL, cleanEpisodeURL, userID).Scan(&episodeID)
if err == nil { // Episode found
// Try to update existing history record
var updateHistoryQuery string
if database.IsPostgreSQLDB() {
updateHistoryQuery = `
UPDATE "UserEpisodeHistory"
SET ListenDuration = $1, ListenDate = $2
WHERE UserID = $3 AND EpisodeID = $4
`
} else {
updateHistoryQuery = `
UPDATE UserEpisodeHistory
SET ListenDuration = ?, ListenDate = ?
WHERE UserID = ? AND EpisodeID = ?
`
}
result, err := tx.Exec(updateHistoryQuery, action.Position, time.Unix(actionTimestamp, 0), userID, episodeID)
if err != nil {
log.Printf("[WARNING] uploadEpisodeActions: Error updating episode history: %v", err)
} else {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
// No history exists, create it
var insertHistoryQuery string
if database.IsPostgreSQLDB() {
insertHistoryQuery = `
INSERT INTO "UserEpisodeHistory"
(UserID, EpisodeID, ListenDuration, ListenDate)
VALUES ($1, $2, $3, $4)
ON CONFLICT (UserID, EpisodeID) DO UPDATE
SET ListenDuration = $3, ListenDate = $4
`
} else {
insertHistoryQuery = `
INSERT INTO UserEpisodeHistory
(UserID, EpisodeID, ListenDuration, ListenDate)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
ListenDuration = VALUES(ListenDuration), ListenDate = VALUES(ListenDate)
`
}
_, err = tx.Exec(insertHistoryQuery, userID, episodeID, action.Position, time.Unix(actionTimestamp, 0))
if err != nil {
log.Printf("[WARNING] uploadEpisodeActions: Error creating episode history: %v", err)
}
}
}
}
}
}
// Commit transaction
if err = tx.Commit(); err != nil {
log.Printf("[ERROR] uploadEpisodeActions: Error committing transaction: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit changes"})
return
}
// Return response
c.JSON(http.StatusOK, models.EpisodeActionResponse{
Timestamp: timestamp,
UpdateURLs: updateURLs,
})
}
}
// getFavoriteEpisodes handles GET /api/2/favorites/{username}.json
func getFavoriteEpisodes(database *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from middleware
userID, _ := c.Get("userID")
// Query for favorite episodes
// Here we identify favorites by checking for episodes with the "is_favorite" setting
var query string
var rows *sql.Rows
var err error
if database.IsPostgreSQLDB() {
query = `
SELECT
e.EpisodeTitle, e.EpisodeURL, e.EpisodeDescription, e.EpisodeArtwork,
p.PodcastName, p.FeedURL, e.EpisodePubDate
FROM "Episodes" e
JOIN "Podcasts" p ON e.PodcastID = p.PodcastID
JOIN "GpodderSyncSettings" s ON s.UserID = p.UserID
AND s.PodcastURL = p.FeedURL
AND s.EpisodeURL = e.EpisodeURL
WHERE s.UserID = $1
AND s.Scope = 'episode'
AND s.SettingKey = 'is_favorite'
AND s.SettingValue = 'true'
ORDER BY e.EpisodePubDate DESC
`
rows, err = database.Query(query, userID)
} else {
query = `
SELECT
e.EpisodeTitle, e.EpisodeURL, e.EpisodeDescription, e.EpisodeArtwork,
p.PodcastName, p.FeedURL, e.EpisodePubDate
FROM Episodes e
JOIN Podcasts p ON e.PodcastID = p.PodcastID
JOIN GpodderSyncSettings s ON s.UserID = p.UserID
AND s.PodcastURL = p.FeedURL
AND s.EpisodeURL = e.EpisodeURL
WHERE s.UserID = ?
AND s.Scope = 'episode'
AND s.SettingKey = 'is_favorite'
AND s.SettingValue = 'true'
ORDER BY e.EpisodePubDate DESC
`
rows, err = database.Query(query, userID)
}
if err != nil {
log.Printf("Error querying favorite episodes: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get favorite episodes"})
return
}
defer rows.Close()
// Build response
favorites := make([]models.Episode, 0)
for rows.Next() {
var episode models.Episode
var pubDate time.Time
if err := rows.Scan(
&episode.Title,
&episode.URL,
&episode.Description,
&episode.Website, // Using EpisodeArtwork for Website for now
&episode.PodcastTitle,
&episode.PodcastURL,
&pubDate,
); err != nil {
log.Printf("Error scanning favorite episode: %v", err)
continue
}
// Format the publication date in ISO 8601
episode.Released = pubDate.Format(time.RFC3339)
// Set MygpoLink (just a placeholder for now)
episode.MygpoLink = fmt.Sprintf("/episode/%s", episode.URL)
favorites = append(favorites, episode)
}
c.JSON(http.StatusOK, favorites)
}
}
// getEpisodeData handles GET /api/2/data/episode.json
// getEpisodeData handles GET /api/2/data/episode.json
func getEpisodeData(database *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
// Parse query parameters
podcastURL := c.Query("podcast")
episodeURL := c.Query("url")
if podcastURL == "" || episodeURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Both podcast and url parameters are required"})
return
}
// Query for episode data
var episode models.Episode
var pubDate time.Time
var query string
if database.IsPostgreSQLDB() {
query = `
SELECT
e.EpisodeTitle, e.EpisodeURL, e.EpisodeDescription, e.EpisodeArtwork,
p.PodcastName, p.FeedURL, e.EpisodePubDate
FROM "Episodes" e
JOIN "Podcasts" p ON e.PodcastID = p.PodcastID
WHERE p.FeedURL = $1 AND e.EpisodeURL = $2
LIMIT 1
`
} else {
query = `
SELECT
e.EpisodeTitle, e.EpisodeURL, e.EpisodeDescription, e.EpisodeArtwork,
p.PodcastName, p.FeedURL, e.EpisodePubDate
FROM Episodes e
JOIN Podcasts p ON e.PodcastID = p.PodcastID
WHERE p.FeedURL = ? AND e.EpisodeURL = ?
LIMIT 1
`
}
err := database.QueryRow(query, podcastURL, episodeURL).Scan(
&episode.Title,
&episode.URL,
&episode.Description,
&episode.Website, // Using EpisodeArtwork for Website for now
&episode.PodcastTitle,
&episode.PodcastURL,
&pubDate,
)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Episode not found"})
} else {
log.Printf("Error querying episode data: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get episode data"})
}
return
}
// Format the publication date in ISO 8601
episode.Released = pubDate.Format(time.RFC3339)
// Set MygpoLink (just a placeholder for now)
episode.MygpoLink = fmt.Sprintf("/episode/%s", episode.URL)
c.JSON(http.StatusOK, episode)
}
}