Files
PinePods-nix/PinePods-0.8.2/mobile/lib/services/pinepods/pinepods_service.dart
2026-03-03 10:57:43 -05:00

2171 lines
62 KiB
Dart

// Create this file at lib/services/pinepods/pinepods_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:pinepods_mobile/entities/episode.dart';
import 'package:pinepods_mobile/entities/pinepods_episode.dart';
import 'package:pinepods_mobile/entities/pinepods_search.dart';
import 'package:pinepods_mobile/entities/user_stats.dart';
import 'package:pinepods_mobile/entities/home_data.dart';
import 'package:pinepods_mobile/entities/podcast.dart';
class PinepodsService {
String? _server;
String? _apiKey;
// Method to initialize with existing credentials
void initializeWithCredentials(String server, String apiKey) {
_server = server;
_apiKey = apiKey;
}
String get apiKey => _apiKey ?? '';
Future<bool> verifyPinepodsInstance(String serverUrl) async {
// Normalize the URL by removing trailing slashes
final normalizedUrl = serverUrl.trim().replaceAll(RegExp(r'/$'), '');
final url = Uri.parse('$normalizedUrl/api/pinepods_check');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['pinepods_instance'] == true;
}
return false;
} catch (e) {
print('Error verifying PinePods instance: $e');
return false;
}
}
Future<bool> login(String serverUrl, String username, String password) async {
// Normalize the URL by removing trailing slashes
final normalizedUrl = serverUrl.trim().replaceAll(RegExp(r'/$'), '');
_server = normalizedUrl;
// Create Basic Auth header
final credentials = base64Encode(utf8.encode('$username:$password'));
final authHeader = 'Basic $credentials';
final url = Uri.parse('$normalizedUrl/api/data/get_key');
try {
final response = await http.get(
url,
headers: {'Authorization': authHeader},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
_apiKey = data['retrieved_key'];
// Verify the API key
return await verifyApiKey();
}
return false;
} catch (e) {
print('Login error: $e');
return false;
}
}
Future<bool> verifyApiKey() async {
if (_server == null || _apiKey == null) {
return false;
}
final url = Uri.parse('$_server/api/data/verify_key');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['status'] == 'success';
}
return false;
} catch (e) {
print('Error verifying API key: $e');
return false;
}
}
// Add method to fetch podcasts from PinePods
Future<List<Map<String, dynamic>>> fetchPodcasts() async {
if (_server == null || _apiKey == null) {
return [];
}
// This endpoint would need to be implemented in your PinePods backend
final url = Uri.parse('$_server/api/data/podcasts');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List;
return data.cast<Map<String, dynamic>>();
}
return [];
} catch (e) {
print('Error fetching podcasts: $e');
return [];
}
}
// Get user's subscribed podcasts using return_pods endpoint
Future<List<Podcast>> getUserPodcasts(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/return_pods/$userId');
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
// User podcasts API response received
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> podsData = data['pods'] ?? [];
List<Podcast> podcasts = [];
for (var podData in podsData) {
// Use episode count from server response
final episodeCount = podData['episodecount'] ?? 0;
// Create placeholder episodes to represent the count
final placeholderEpisodes = List.generate(
episodeCount,
(index) => Episode(
guid: 'placeholder_$index',
podcast: podData['podcastname'] ?? '',
title: 'Episode ${index + 1}',
),
);
podcasts.add(
Podcast(
id: podData['podcastid'],
title: podData['podcastname'] ?? '',
description: podData['description'] ?? '',
imageUrl: podData['artworkurl'] ?? '',
thumbImageUrl: podData['artworkurl'] ?? '',
url: podData['feedurl'] ?? '',
link: podData['websiteurl'] ?? '',
copyright: podData['author'] ?? '',
guid: podData['feedurl'] ?? '',
episodes: placeholderEpisodes,
),
);
}
return podcasts;
} else {
throw Exception('Failed to get user podcasts: ${response.statusCode}');
}
} catch (e) {
print('Error getting user podcasts: $e');
rethrow;
}
}
// Get recent episodes (last 30 days)
Future<List<PinepodsEpisode>> getRecentEpisodes(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated - server or API key missing');
}
final url = Uri.parse('$_server/api/data/return_episodes/$userId');
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final responseText = response.body;
final data = jsonDecode(responseText);
// Handle the response structure from the web implementation
if (data is Map && data['episodes'] != null) {
final episodesList = data['episodes'] as List;
return episodesList
.map((episode) => PinepodsEpisode.fromJson(episode))
.toList();
} else if (data is List) {
// Handle direct list response
return data
.map((episode) => PinepodsEpisode.fromJson(episode))
.toList();
} else {
return [];
}
} else {
throw Exception(
'Failed to fetch recent episodes: ${response.statusCode} ${response.reasonPhrase}',
);
}
} catch (e) {
print('Error fetching recent episodes: $e');
throw Exception('Error fetching recent episodes: $e');
}
}
// Set credentials (used when user logs in)
void setCredentials(String server, String apiKey) {
_server = server.trim().replaceAll(RegExp(r'/$'), '');
_apiKey = apiKey;
}
// Check if user is authenticated
bool get isAuthenticated => _server != null && _apiKey != null;
// Get server URL
String? get server => _server;
// Check if episode exists in database
Future<bool> checkEpisodeInDb(
int userId,
String episodeTitle,
String episodeUrl,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/check_episode_in_db');
try {
final requestBody = jsonEncode({
'user_id': userId,
'episode_title': episodeTitle,
'episode_url': episodeUrl,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['exists'] == true;
}
return false;
} catch (e) {
print('Error checking episode in DB: $e');
return false;
}
}
// Get episode ID from title and URL
Future<int> getEpisodeId(
int userId,
String episodeTitle,
String episodeUrl,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse(
'$_server/api/data/get_episode_id_ep_name?user_id=$userId&episode_url=${Uri.encodeComponent(episodeUrl)}&episode_title=${Uri.encodeComponent(episodeTitle)}&is_youtube=$isYoutube',
);
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
// Parse the response as a plain integer
final episodeId = int.tryParse(response.body.trim()) ?? 0;
return episodeId;
}
return 0;
} catch (e) {
print('Error getting episode ID: $e');
return 0;
}
}
// Add episode to history
Future<bool> addHistory(
int episodeId,
double episodePos,
int userId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/record_podcast_history');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'episode_pos': episodePos,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
// History API response received
return response.statusCode == 200;
} catch (e) {
print('Error adding history: $e');
return false;
}
}
// Queue episode
Future<bool> queueEpisode(int episodeId, int userId, bool isYoutube) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/queue_pod');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
// Queue API response received
return response.statusCode == 200;
} catch (e) {
print('Error queueing episode: $e');
return false;
}
}
// Increment played count
Future<bool> incrementPlayed(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/increment_played/$userId');
print('Making API call to: $url');
try {
final response = await http.put(url, headers: {'Api-Key': _apiKey!});
print(
'Increment played response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error incrementing played: $e');
return false;
}
}
// Get podcast ID from episode
Future<int> getPodcastIdFromEpisode(
int episodeId,
int userId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse(
'$_server/api/data/get_podcast_id_from_ep/$episodeId',
);
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['podcast_id'] ?? 0;
}
return 0;
} catch (e) {
print('Error getting podcast ID: $e');
return 0;
}
}
// Get play episode details (playback speed, skip times)
Future<PlayEpisodeDetails> getPlayEpisodeDetails(
int userId,
int podcastId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_play_episode_details');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'user_id': userId,
'podcast_id': podcastId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Play episode details response: ${response.statusCode} - ${response.body}',
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return PlayEpisodeDetails(
playbackSpeed: (data['playback_speed'] as num?)?.toDouble() ?? 1.0,
startSkip: data['start_skip'] ?? 0,
endSkip: data['end_skip'] ?? 0,
);
}
return PlayEpisodeDetails(playbackSpeed: 1.0, startSkip: 0, endSkip: 0);
} catch (e) {
print('Error getting play episode details: $e');
return PlayEpisodeDetails(playbackSpeed: 1.0, startSkip: 0, endSkip: 0);
}
}
// Record listen duration for episode
Future<bool> recordListenDuration(
int episodeId,
int userId,
double listenDuration,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/record_listen_duration');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'listen_duration': listenDuration,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Record listen duration response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error recording listen duration: $e');
return false;
}
}
// Increment listen time for user stats
Future<bool> incrementListenTime(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/increment_listen_time/$userId');
print('Making API call to: $url');
try {
final response = await http.put(url, headers: {'Api-Key': _apiKey!});
print(
'Increment listen time response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error incrementing listen time: $e');
return false;
}
}
// Save episode
Future<bool> saveEpisode(int episodeId, int userId, bool isYoutube) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/save_episode');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
// Save episode API response received
return response.statusCode == 200;
} catch (e) {
print('Error saving episode: $e');
return false;
}
}
// Remove saved episode
Future<bool> removeSavedEpisode(
int episodeId,
int userId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/remove_saved_episode');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Remove saved episode response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error removing saved episode: $e');
return false;
}
}
// Download episode to server
Future<bool> downloadEpisode(
int episodeId,
int userId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/download_podcast');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Download episode response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error downloading episode: $e');
return false;
}
}
// Delete downloaded episode from server
Future<bool> deleteEpisode(int episodeId, int userId, bool isYoutube) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/delete_episode');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Delete episode response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error deleting episode: $e');
return false;
}
}
// Mark episode as completed
Future<bool> markEpisodeCompleted(
int episodeId,
int userId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/mark_episode_completed');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Mark completed response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error marking episode completed: $e');
return false;
}
}
// Mark episode as uncompleted
Future<bool> markEpisodeUncompleted(
int episodeId,
int userId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/mark_episode_uncompleted');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Mark uncompleted response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error marking episode uncompleted: $e');
return false;
}
}
// Remove episode from queue
Future<bool> removeQueuedEpisode(
int episodeId,
int userId,
bool isYoutube,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/remove_queued_pod');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
print(
'Remove queued response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error removing queued episode: $e');
return false;
}
}
// Get user history
Future<List<PinepodsEpisode>> getUserHistory(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/user_history/$userId');
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
// User history API response received
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final episodesList = data['data'] as List<dynamic>? ?? [];
return episodesList.map((episodeData) {
return PinepodsEpisode(
podcastName: episodeData['podcastname'] ?? '',
episodeTitle: episodeData['episodetitle'] ?? '',
episodePubDate: episodeData['episodepubdate'] ?? '',
episodeDescription: episodeData['episodedescription'] ?? '',
episodeArtwork: episodeData['episodeartwork'] ?? '',
episodeUrl: episodeData['episodeurl'] ?? '',
episodeDuration: episodeData['episodeduration'] ?? 0,
listenDuration: episodeData['listenduration'] ?? 0,
episodeId: episodeData['episodeid'] ?? 0,
completed: episodeData['completed'] ?? false,
saved: episodeData['saved'] ?? false,
queued: episodeData['queued'] ?? false,
downloaded: episodeData['downloaded'] ?? false,
isYoutube: episodeData['is_youtube'] ?? false,
);
}).toList();
} else {
throw Exception('Failed to load user history: ${response.statusCode}');
}
} catch (e) {
print('Error getting user history: $e');
rethrow;
}
}
// Get queued episodes
Future<List<PinepodsEpisode>> getQueuedEpisodes(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse(
'$_server/api/data/get_queued_episodes?user_id=$userId',
);
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
print(
'Queued episodes response: ${response.statusCode} - ${response.body}',
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final episodesList = data['data'] as List<dynamic>? ?? [];
return episodesList.map((episodeData) {
return PinepodsEpisode(
podcastName: episodeData['podcastname'] ?? '',
episodeTitle: episodeData['episodetitle'] ?? '',
episodePubDate: episodeData['episodepubdate'] ?? '',
episodeDescription: episodeData['episodedescription'] ?? '',
episodeArtwork: episodeData['episodeartwork'] ?? '',
episodeUrl: episodeData['episodeurl'] ?? '',
episodeDuration: episodeData['episodeduration'] ?? 0,
listenDuration: episodeData['listenduration'] ?? 0,
episodeId: episodeData['episodeid'] ?? 0,
completed: episodeData['completed'] ?? false,
saved: episodeData['saved'] ?? false,
queued:
episodeData['queued'] ??
true, // Should always be true for queued episodes
downloaded: episodeData['downloaded'] ?? false,
isYoutube: episodeData['is_youtube'] ?? false,
);
}).toList();
} else {
throw Exception(
'Failed to load queued episodes: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting queued episodes: $e');
rethrow;
}
}
// Get saved episodes
Future<List<PinepodsEpisode>> getSavedEpisodes(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/saved_episode_list/$userId');
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
// Saved episodes API response received
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final episodesList = data['saved_episodes'] as List<dynamic>? ?? [];
return episodesList.map((episodeData) {
return PinepodsEpisode(
podcastName: episodeData['podcastname'] ?? '',
episodeTitle: episodeData['episodetitle'] ?? '',
episodePubDate: episodeData['episodepubdate'] ?? '',
episodeDescription: episodeData['episodedescription'] ?? '',
episodeArtwork: episodeData['episodeartwork'] ?? '',
episodeUrl: episodeData['episodeurl'] ?? '',
episodeDuration: episodeData['episodeduration'] ?? 0,
listenDuration: episodeData['listenduration'] ?? 0,
episodeId: episodeData['episodeid'] ?? 0,
completed: episodeData['completed'] ?? false,
saved:
episodeData['saved'] ??
true, // Should always be true for saved episodes
queued: episodeData['queued'] ?? false,
downloaded: episodeData['downloaded'] ?? false,
isYoutube: episodeData['is_youtube'] ?? false,
);
}).toList();
} else {
throw Exception(
'Failed to load saved episodes: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting saved episodes: $e');
rethrow;
}
}
// Get episode metadata
Future<PinepodsEpisode?> getEpisodeMetadata(
int episodeId,
int userId, {
bool isYoutube = false,
bool personEpisode = false,
}) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_episode_metadata');
try {
final requestBody = jsonEncode({
'episode_id': episodeId,
'user_id': userId,
'person_episode': personEpisode,
'is_youtube': isYoutube,
});
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final episodeData = data['episode'];
return PinepodsEpisode(
podcastName: episodeData['podcastname'] ?? '',
episodeTitle: episodeData['episodetitle'] ?? '',
episodePubDate: episodeData['episodepubdate'] ?? '',
episodeDescription: episodeData['episodedescription'] ?? '',
episodeArtwork: episodeData['episodeartwork'] ?? '',
episodeUrl: episodeData['episodeurl'] ?? '',
episodeDuration: episodeData['episodeduration'] ?? 0,
listenDuration: episodeData['listenduration'] ?? 0,
episodeId: episodeData['episodeid'] ?? episodeId,
completed: episodeData['completed'] ?? false,
saved: episodeData['is_saved'] ?? false,
queued: episodeData['is_queued'] ?? false,
downloaded: episodeData['is_downloaded'] ?? false,
isYoutube: episodeData['is_youtube'] ?? isYoutube,
podcastId: episodeData['podcastid'],
);
}
return null;
} catch (e) {
print('Error getting episode metadata: $e');
return null;
}
}
// Get downloaded episodes from server
Future<List<PinepodsEpisode>> getServerDownloads(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse(
'$_server/api/data/download_episode_list?user_id=$userId',
);
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final episodesList =
data['downloaded_episodes'] as List<dynamic>? ?? [];
return episodesList.map((episodeData) {
return PinepodsEpisode(
podcastName: episodeData['podcastname'] ?? '',
episodeTitle: episodeData['episodetitle'] ?? '',
episodePubDate: episodeData['episodepubdate'] ?? '',
episodeDescription: episodeData['episodedescription'] ?? '',
episodeArtwork: episodeData['episodeartwork'] ?? '',
episodeUrl: episodeData['episodeurl'] ?? '',
episodeDuration: episodeData['episodeduration'] ?? 0,
listenDuration: episodeData['listenduration'] ?? 0,
episodeId: episodeData['episodeid'] ?? 0,
completed: episodeData['completed'] ?? false,
saved: episodeData['saved'] ?? false,
queued: episodeData['queued'] ?? false,
downloaded:
episodeData['downloaded'] ??
true, // Should always be true for downloaded episodes
isYoutube: episodeData['is_youtube'] ?? false,
);
}).toList();
} else {
throw Exception(
'Failed to load server downloads: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting server downloads: $e');
rethrow;
}
}
// Get stream URL for episode
String getStreamUrl(
int episodeId,
int userId, {
bool isYoutube = false,
bool isLocal = false,
}) {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
if (isYoutube) {
return '$_server/api/data/stream/$episodeId?api_key=$_apiKey&user_id=$userId&type=youtube';
} else if (isLocal) {
return '$_server/api/data/stream/$episodeId?api_key=$_apiKey&user_id=$userId';
} else {
// For external episodes, return the original URL
return '';
}
}
// Search for podcasts using PinePods search API
Future<PinepodsSearchResult> searchPodcasts(
String query,
SearchProvider provider,
) async {
const searchApiUrl = 'https://search.pinepods.online';
final url = Uri.parse(
'$searchApiUrl/api/search?query=${Uri.encodeComponent(query)}&index=${provider.value}',
);
try {
print('Making search request to: $url');
final response = await http.get(url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// Search API response received
return PinepodsSearchResult.fromJson(data);
} else {
throw Exception('Failed to search podcasts: ${response.statusCode}');
}
} catch (e) {
print('Error searching podcasts: $e');
rethrow;
}
}
// Check if a podcast is already added to the server
Future<bool> checkPodcastExists(
String podcastTitle,
String podcastUrl,
int userId,
) async {
if (_server == null || _apiKey == null) {
return false;
}
final url = Uri.parse('$_server/api/data/check_podcast').replace(
queryParameters: {
'user_id': userId.toString(),
'podcast_name': podcastTitle,
'podcast_url': podcastUrl,
},
);
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['exists'] == true;
}
return false;
} catch (e) {
print('Error checking podcast exists: $e');
return false;
}
}
// Add a podcast to the server
Future<bool> addPodcast(UnifiedPinepodsPodcast podcast, int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/add_podcast');
final body = {
'podcast_values': {
'pod_title': podcast.title,
'pod_artwork': podcast.artwork,
'pod_author': podcast.author,
'categories': podcast.categories ?? {},
'pod_description': podcast.description,
'pod_episode_count': podcast.episodeCount,
'pod_feed_url': podcast.url,
'pod_website': podcast.link,
'pod_explicit': podcast.explicit,
'user_id': userId,
},
'podcast_index_id': podcast.indexId,
};
try {
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: jsonEncode(body),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['success'] == true;
}
return false;
} catch (e) {
print('Error adding podcast: $e');
rethrow;
}
}
// Remove a podcast from the server
Future<bool> removePodcast(
String podcastTitle,
String podcastUrl,
int userId,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/remove_podcast');
final body = {
'podcast_name': podcastTitle,
'podcast_url': podcastUrl,
'user_id': userId,
};
try {
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: jsonEncode(body),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['success'] == true;
}
return false;
} catch (e) {
print('Error removing podcast: $e');
rethrow;
}
}
// Get podcast details dynamically (whether added or not)
Future<PodcastDetailsData> getPodcastDetailsDynamic({
required int userId,
required String podcastTitle,
required String podcastUrl,
required int podcastIndexId,
required bool added,
bool displayOnly = false,
}) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_podcast_details_dynamic')
.replace(
queryParameters: {
'user_id': userId.toString(),
'podcast_title': podcastTitle,
'podcast_url': podcastUrl,
'podcast_index_id': podcastIndexId.toString(),
'added': added.toString(),
'display_only': displayOnly.toString(),
},
);
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// Podcast details API response received
return PodcastDetailsData.fromJson(data);
} else {
throw Exception(
'Failed to get podcast details: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting podcast details: $e');
rethrow;
}
}
// Get podcast details by podcast ID (for subscribed podcasts)
Future<Map<String, dynamic>?> getPodcastDetailsById(
int podcastId,
int userId,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_podcast_details').replace(
queryParameters: {
'podcast_id': podcastId.toString(),
'user_id': userId.toString(),
},
);
try {
print('Getting podcast details by ID from: $url');
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// Podcast details by ID API response received
return data['details'];
} else {
throw Exception(
'Failed to get podcast details: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting podcast details by ID: $e');
rethrow;
}
}
// Get podcast ID by feed URL and title
Future<int?> getPodcastId(
int userId,
String podcastFeed,
String podcastTitle,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_podcast_id').replace(
queryParameters: {
'user_id': userId.toString(),
'podcast_feed': podcastFeed,
'podcast_title': podcastTitle,
},
);
try {
print('Getting podcast ID from: $url');
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// Podcast ID API response received
final podcastId = data['podcast_id'];
if (podcastId is int) {
return podcastId;
}
return null;
} else {
throw Exception('Failed to get podcast ID: ${response.statusCode}');
}
} catch (e) {
print('Error getting podcast ID: $e');
return null;
}
}
// Get episodes for an added podcast
Future<List<PinepodsEpisode>> getPodcastEpisodes(
int userId,
int podcastId,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/podcast_episodes').replace(
queryParameters: {
'user_id': userId.toString(),
'podcast_id': podcastId.toString(),
},
);
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final episodes = data['episodes'] as List;
return episodes.map((episodeData) {
// Add default values only for fields not provided by this endpoint
final episodeWithDefaults = Map<String, dynamic>.from(episodeData);
// Only add defaults if these fields are not present in the API response
episodeWithDefaults['saved'] ??= false;
episodeWithDefaults['queued'] ??= false;
episodeWithDefaults['downloaded'] ??= false;
episodeWithDefaults['is_youtube'] ??= false;
return PinepodsEpisode.fromJson(episodeWithDefaults);
}).toList();
} else {
throw Exception(
'Failed to get podcast episodes: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting podcast episodes: $e');
rethrow;
}
}
// Get user statistics
Future<UserStats> getUserStats(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse(
'$_server/api/data/get_stats',
).replace(queryParameters: {'user_id': userId.toString()});
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return UserStats.fromJson(data);
} else {
throw Exception('Failed to get user stats: ${response.statusCode}');
}
} catch (e) {
print('Error getting user stats: $e');
rethrow;
}
}
// Get PinePods version
Future<String> getPinepodsVersion() async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_pinepods_version');
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['data'] ?? 'Unknown';
} else {
throw Exception(
'Failed to get PinePods version: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting PinePods version: $e');
return 'Unknown';
}
}
// Get user details by user ID
Future<Map<String, dynamic>?> getUserDetails(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/user_details_id/$userId');
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data;
} else {
throw Exception('Failed to get user details: ${response.statusCode}');
}
} catch (e) {
print('Error getting user details: $e');
return null;
}
}
// Get user ID from API key
Future<int?> getUserIdFromApiKey() async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/id_from_api_key');
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final userId = int.tryParse(response.body.trim());
return userId;
} else {
throw Exception('Failed to get user ID: ${response.statusCode}');
}
} catch (e) {
print('Error getting user ID: $e');
return null;
}
}
// Get home overview data
Future<HomeOverview> getHomeOverview(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/home_overview?user_id=$userId');
print('Making API call to: $url');
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return HomeOverview.fromJson(data);
} else {
throw Exception('Failed to load home overview: ${response.statusCode}');
}
} catch (e) {
print('Error getting home overview: $e');
rethrow;
}
}
// Get playlists
Future<PlaylistResponse> getPlaylists(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_playlists?user_id=$userId');
print('Making API call to: $url');
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
// Playlists API response received
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return PlaylistResponse.fromJson(data);
} else {
throw Exception('Failed to load playlists: ${response.statusCode}');
}
} catch (e) {
print('Error getting playlists: $e');
rethrow;
}
}
// Get user theme
Future<String?> getUserTheme(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_theme/$userId');
print('Making API call to: $url');
try {
final response = await http.get(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
);
// Theme API response received
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['theme'] as String?;
} else {
throw Exception('Failed to get user theme: ${response.statusCode}');
}
} catch (e) {
print('Error getting user theme: $e');
return null;
}
}
// Set user theme
Future<bool> setUserTheme(int userId, String theme) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/user/set_theme');
print('Making API call to: $url');
try {
final requestBody = jsonEncode({'user_id': userId, 'new_theme': theme});
final response = await http.put(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: requestBody,
);
// Set theme API response received
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['message'] != null;
} else {
throw Exception('Failed to set user theme: ${response.statusCode}');
}
} catch (e) {
print('Error setting user theme: $e');
return false;
}
}
// Get user playlists
Future<List<PlaylistData>> getUserPlaylists(int userId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/get_playlists?user_id=$userId');
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
print(
'Get playlists response: ${response.statusCode} - ${response.body}',
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> playlistsData = data['playlists'] ?? [];
List<PlaylistData> playlists = [];
for (var playlistData in playlistsData) {
playlists.add(PlaylistData.fromJson(playlistData));
}
return playlists;
} else {
throw Exception('Failed to get playlists: ${response.statusCode}');
}
} catch (e) {
print('Error getting playlists: $e');
rethrow;
}
}
// Create playlist
Future<bool> createPlaylist(CreatePlaylistRequest request) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/create_playlist');
print('Making API call to: $url');
try {
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: jsonEncode(request.toJson()),
);
print(
'Create playlist response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error creating playlist: $e');
return false;
}
}
// Delete playlist
Future<bool> deletePlaylist(int userId, int playlistId) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/delete_playlist');
print('Making API call to: $url');
try {
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: jsonEncode({'user_id': userId, 'playlist_id': playlistId}),
);
print(
'Delete playlist response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error deleting playlist: $e');
return false;
}
}
// Get playlist episodes
Future<PlaylistEpisodesResponse> getPlaylistEpisodes(
int userId,
int playlistId,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse(
'$_server/api/data/get_playlist_episodes?user_id=$userId&playlist_id=$playlistId',
);
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
print(
'Get playlist episodes response: ${response.statusCode} - ${response.body}',
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return PlaylistEpisodesResponse.fromJson(data);
} else {
throw Exception(
'Failed to get playlist episodes: ${response.statusCode}',
);
}
} catch (e) {
print('Error getting playlist episodes: $e');
rethrow;
}
}
// Reorder queue episodes
Future<bool> reorderQueue(int userId, List<int> episodeIds) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/reorder_queue?user_id=$userId');
print('Making API call to: $url');
try {
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: jsonEncode({'episode_ids': episodeIds}),
);
print(
'Reorder queue response: ${response.statusCode} - ${response.body}',
);
return response.statusCode == 200;
} catch (e) {
print('Error reordering queue: $e');
return false;
}
}
// Search episodes in user's subscriptions
Future<List<SearchEpisodeResult>> searchEpisodes(
int userId,
String searchTerm,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/search_data');
print('Making API call to: $url');
try {
final response = await http.post(
url,
headers: {'Api-Key': _apiKey!, 'Content-Type': 'application/json'},
body: jsonEncode({'search_term': searchTerm, 'user_id': userId}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> episodesData = data['data'] ?? [];
List<SearchEpisodeResult> episodes = [];
for (var episodeData in episodesData) {
episodes.add(SearchEpisodeResult.fromJson(episodeData));
}
return episodes;
} else {
throw Exception('Failed to search episodes: ${response.statusCode}');
}
} catch (e) {
print('Error searching episodes: $e');
rethrow;
}
}
// Fetch podcast 2.0 data for a specific episode
Future<Map<String, dynamic>?> fetchPodcasting2Data(
int episodeId,
int userId,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/fetch_podcasting_2_data').replace(
queryParameters: {
'episode_id': episodeId.toString(),
'user_id': userId.toString(),
},
);
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
print(
'Podcast 2.0 data response: ${response.statusCode} - ${response.body}',
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data;
} else {
print('Failed to fetch podcast 2.0 data: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error fetching podcast 2.0 data: $e');
return null;
}
}
// Fetch podcast 2.0 data for a specific podcast
Future<Map<String, dynamic>?> fetchPodcasting2PodData(
int podcastId,
int userId,
) async {
if (_server == null || _apiKey == null) {
throw Exception('Not authenticated');
}
final url = Uri.parse('$_server/api/data/fetch_podcasting_2_pod_data')
.replace(
queryParameters: {
'podcast_id': podcastId.toString(),
'user_id': userId.toString(),
},
);
print('Making API call to: $url');
try {
final response = await http.get(url, headers: {'Api-Key': _apiKey!});
print(
'Podcast 2.0 pod data response: ${response.statusCode} - ${response.body}',
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data;
} else {
print('Failed to fetch podcast 2.0 pod data: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error fetching podcast 2.0 pod data: $e');
return null;
}
}
}
class PodcastDetailsData {
final int podcastId;
final String podcastName;
final String feedUrl;
final String description;
final String author;
final String artworkUrl;
final bool explicit;
final int episodeCount;
final Map<String, String>? categories;
final String websiteUrl;
final int podcastIndexId;
final bool isYoutube;
PodcastDetailsData({
required this.podcastId,
required this.podcastName,
required this.feedUrl,
required this.description,
required this.author,
required this.artworkUrl,
required this.explicit,
required this.episodeCount,
this.categories,
required this.websiteUrl,
required this.podcastIndexId,
required this.isYoutube,
});
factory PodcastDetailsData.fromJson(Map<String, dynamic> json) {
return PodcastDetailsData(
podcastId: json['podcastid'] ?? 0,
podcastName: json['podcastname'] ?? '',
feedUrl: json['feedurl'] ?? '',
description: json['description'] ?? '',
author: json['author'] ?? '',
artworkUrl: json['artworkurl'] ?? '',
explicit: json['explicit'] ?? false,
episodeCount: json['episodecount'] ?? 0,
categories: json['categories'] != null
? Map<String, String>.from(json['categories'] as Map)
: null,
websiteUrl: json['websiteurl'] ?? '',
podcastIndexId: json['podcastindexid'] ?? 0,
isYoutube: json['is_youtube'] ?? false,
);
}
}
class PlayEpisodeDetails {
final double playbackSpeed;
final int startSkip;
final int endSkip;
PlayEpisodeDetails({
required this.playbackSpeed,
required this.startSkip,
required this.endSkip,
});
}
// Playlist Data Classes
class PlaylistData {
final int playlistId;
final int userId;
final String name;
final String? description;
final bool isSystemPlaylist;
final List<int>? podcastIds;
final bool includeUnplayed;
final bool includePartiallyPlayed;
final bool includePlayed;
final int? minDuration;
final int? maxDuration;
final String sortOrder;
final bool groupByPodcast;
final int? maxEpisodes;
final String lastUpdated;
final String created;
final int? episodeCount;
final String? iconName;
PlaylistData({
required this.playlistId,
required this.userId,
required this.name,
this.description,
required this.isSystemPlaylist,
this.podcastIds,
required this.includeUnplayed,
required this.includePartiallyPlayed,
required this.includePlayed,
this.minDuration,
this.maxDuration,
required this.sortOrder,
required this.groupByPodcast,
this.maxEpisodes,
required this.lastUpdated,
required this.created,
this.episodeCount,
this.iconName,
});
factory PlaylistData.fromJson(Map<String, dynamic> json) {
return PlaylistData(
playlistId: json['playlist_id'] ?? 0,
userId: json['user_id'] ?? 0,
name: json['name'] ?? '',
description: json['description'],
isSystemPlaylist: json['is_system_playlist'] ?? false,
podcastIds: json['podcast_ids'] != null
? List<int>.from(json['podcast_ids'])
: null,
includeUnplayed: json['include_unplayed'] ?? true,
includePartiallyPlayed: json['include_partially_played'] ?? true,
includePlayed: json['include_played'] ?? false,
minDuration: json['min_duration'],
maxDuration: json['max_duration'],
sortOrder: json['sort_order'] ?? 'date_desc',
groupByPodcast: json['group_by_podcast'] ?? false,
maxEpisodes: json['max_episodes'],
lastUpdated: json['last_updated'] ?? '',
created: json['created'] ?? '',
episodeCount: json['episode_count'],
iconName: json['icon_name'],
);
}
}
class CreatePlaylistRequest {
final int userId;
final String name;
final String? description;
final List<int>? podcastIds;
final bool includeUnplayed;
final bool includePartiallyPlayed;
final bool includePlayed;
final int? minDuration;
final int? maxDuration;
final String sortOrder;
final bool groupByPodcast;
final int? maxEpisodes;
final String iconName;
final double? playProgressMin;
final double? playProgressMax;
final int? timeFilterHours;
CreatePlaylistRequest({
required this.userId,
required this.name,
this.description,
this.podcastIds,
required this.includeUnplayed,
required this.includePartiallyPlayed,
required this.includePlayed,
this.minDuration,
this.maxDuration,
required this.sortOrder,
required this.groupByPodcast,
this.maxEpisodes,
required this.iconName,
this.playProgressMin,
this.playProgressMax,
this.timeFilterHours,
});
Map<String, dynamic> toJson() {
return {
'user_id': userId,
'name': name,
'description': description,
'podcast_ids': podcastIds,
'include_unplayed': includeUnplayed,
'include_partially_played': includePartiallyPlayed,
'include_played': includePlayed,
'min_duration': minDuration,
'max_duration': maxDuration,
'sort_order': sortOrder,
'group_by_podcast': groupByPodcast,
'max_episodes': maxEpisodes,
'icon_name': iconName,
'play_progress_min': playProgressMin,
'play_progress_max': playProgressMax,
'time_filter_hours': timeFilterHours,
};
}
}
class PlaylistEpisodesResponse {
final List<PinepodsEpisode> episodes;
final PlaylistInfo playlistInfo;
PlaylistEpisodesResponse({
required this.episodes,
required this.playlistInfo,
});
factory PlaylistEpisodesResponse.fromJson(Map<String, dynamic> json) {
return PlaylistEpisodesResponse(
episodes: (json['episodes'] as List<dynamic>? ?? [])
.map((e) => PinepodsEpisode.fromJson(e))
.toList(),
playlistInfo: PlaylistInfo.fromJson(json['playlist_info'] ?? {}),
);
}
}
class PlaylistInfo {
final String name;
final String? description;
final int? episodeCount;
final String? iconName;
PlaylistInfo({
required this.name,
this.description,
this.episodeCount,
this.iconName,
});
factory PlaylistInfo.fromJson(Map<String, dynamic> json) {
return PlaylistInfo(
name: json['name'] ?? '',
description: json['description'],
episodeCount: json['episode_count'],
iconName: json['icon_name'],
);
}
}
class SearchEpisodeResult {
final int podcastId;
final String podcastName;
final String artworkUrl;
final String author;
final String categories;
final String description;
final int? episodeCount;
final String feedUrl;
final String websiteUrl;
final bool explicit;
final int userId;
final int episodeId;
final String episodeTitle;
final String episodeDescription;
final String episodePubDate;
final String episodeArtwork;
final String episodeUrl;
final int episodeDuration;
final bool completed;
final bool saved;
final bool queued;
final bool downloaded;
final bool isYoutube;
final int? listenDuration;
SearchEpisodeResult({
required this.podcastId,
required this.podcastName,
required this.artworkUrl,
required this.author,
required this.categories,
required this.description,
this.episodeCount,
required this.feedUrl,
required this.websiteUrl,
required this.explicit,
required this.userId,
required this.episodeId,
required this.episodeTitle,
required this.episodeDescription,
required this.episodePubDate,
required this.episodeArtwork,
required this.episodeUrl,
required this.episodeDuration,
required this.completed,
required this.saved,
required this.queued,
required this.downloaded,
required this.isYoutube,
this.listenDuration,
});
factory SearchEpisodeResult.fromJson(Map<String, dynamic> json) {
return SearchEpisodeResult(
podcastId: json['podcastid'] ?? 0,
podcastName: json['podcastname'] ?? '',
artworkUrl: json['artworkurl'] ?? '',
author: json['author'] ?? '',
categories: _parseCategories(json['categories']),
description: json['description'] ?? '',
episodeCount: json['episodecount'],
feedUrl: json['feedurl'] ?? '',
websiteUrl: json['websiteurl'] ?? '',
explicit: (json['explicit'] ?? 0) == 1,
userId: json['userid'] ?? 0,
episodeId: json['episodeid'] ?? 0,
episodeTitle: json['episodetitle'] ?? '',
episodeDescription: json['episodedescription'] ?? '',
episodePubDate: json['episodepubdate'] ?? '',
episodeArtwork: json['episodeartwork'] ?? '',
episodeUrl: json['episodeurl'] ?? '',
episodeDuration: json['episodeduration'] ?? 0,
completed: json['completed'] ?? false,
saved: json['saved'] ?? false,
queued: json['queued'] ?? false,
downloaded: json['downloaded'] ?? false,
isYoutube: json['is_youtube'] ?? false,
listenDuration: json['listenduration'],
);
}
// Convert to PinepodsEpisode for compatibility with existing widgets
PinepodsEpisode toPinepodsEpisode() {
return PinepodsEpisode(
podcastName: podcastName,
episodeTitle: episodeTitle,
episodePubDate: episodePubDate,
episodeDescription: episodeDescription,
episodeArtwork: episodeArtwork.isNotEmpty ? episodeArtwork : artworkUrl,
episodeUrl: episodeUrl,
episodeDuration: episodeDuration,
listenDuration: listenDuration,
episodeId: episodeId,
completed: completed,
saved: saved,
queued: queued,
downloaded: downloaded,
isYoutube: isYoutube,
);
}
/// Parse categories from either string or Map format
static String _parseCategories(dynamic categories) {
if (categories == null) return '';
if (categories is String) {
// Old format - return as is
return categories;
} else if (categories is Map<String, dynamic>) {
// New format - convert map values to comma-separated string
if (categories.isEmpty) return '';
return categories.values.join(', ');
}
return '';
}
}