added cargo files

This commit is contained in:
2026-03-03 10:57:43 -05:00
parent 478a90e01b
commit 169df46bc2
813 changed files with 227273 additions and 9 deletions

View File

@@ -0,0 +1,247 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:pinepods_mobile/api/podcast/podcast_api.dart';
import 'package:pinepods_mobile/core/environment.dart';
import 'package:pinepods_mobile/entities/transcript.dart';
import 'package:flutter/foundation.dart';
import 'package:podcast_search/podcast_search.dart' as podcast_search;
import 'package:http/http.dart' as http;
import 'package:html/parser.dart' as html;
/// An implementation of the [PodcastApi].
///
/// A simple wrapper class that interacts with the iTunes/PodcastIndex search API
/// via the podcast_search package.
class MobilePodcastApi extends PodcastApi {
/// Set when using a custom certificate authority.
SecurityContext? _defaultSecurityContext;
/// Bytes containing a custom certificate authority.
List<int> _certificateAuthorityBytes = [];
@override
Future<podcast_search.SearchResult> search(
String term, {
String? country,
String? attribute,
int? limit,
String? language,
int version = 0,
bool explicit = false,
String? searchProvider,
}) async {
var searchParams = {
'term': term,
'searchProvider': searchProvider,
};
return compute(_search, searchParams);
}
@override
Future<podcast_search.SearchResult> charts({
int? size = 20,
String? genre,
String? searchProvider,
String? countryCode = '',
String? languageCode = '',
}) async {
var searchParams = {
'size': size.toString(),
'genre': genre,
'searchProvider': searchProvider,
'countryCode': countryCode,
'languageCode': languageCode,
};
return compute(_charts, searchParams);
}
@override
List<String> genres(String searchProvider) {
var provider = searchProvider == 'itunes'
? const podcast_search.ITunesProvider()
: podcast_search.PodcastIndexProvider(
key: podcastIndexKey,
secret: podcastIndexSecret,
);
return podcast_search.Search(
userAgent: Environment.userAgent(),
searchProvider: provider,
).genres();
}
@override
Future<podcast_search.Podcast> loadFeed(String url) async {
return _loadFeed(url);
}
@override
Future<podcast_search.Chapters> loadChapters(String url) async {
// In podcast_search 0.7.11, load chapters using Feed.loadChaptersByUrl
try {
return await podcast_search.Feed.loadChaptersByUrl(url: url);
} catch (e) {
// Fallback: create empty chapters if loading fails
return podcast_search.Chapters(url: url);
}
}
@override
Future<podcast_search.Transcript> loadTranscript(TranscriptUrl transcriptUrl) async {
// Handle HTML transcripts with custom parser
if (transcriptUrl.type == TranscriptFormat.html) {
return await _loadHtmlTranscript(transcriptUrl);
}
late podcast_search.TranscriptFormat format;
switch (transcriptUrl.type) {
case TranscriptFormat.subrip:
format = podcast_search.TranscriptFormat.subrip;
break;
case TranscriptFormat.json:
format = podcast_search.TranscriptFormat.json;
break;
case TranscriptFormat.html:
// This case is now handled above
format = podcast_search.TranscriptFormat.unsupported;
break;
case TranscriptFormat.unsupported:
format = podcast_search.TranscriptFormat.unsupported;
break;
}
// In podcast_search 0.7.11, load transcript using Feed.loadTranscriptByUrl
try {
// Create a podcast_search.TranscriptUrl from our local TranscriptUrl
final searchTranscriptUrl = podcast_search.TranscriptUrl(
url: transcriptUrl.url,
type: format,
language: transcriptUrl.language ?? '',
rel: transcriptUrl.rel ?? '',
);
return await podcast_search.Feed.loadTranscriptByUrl(
transcriptUrl: searchTranscriptUrl
);
} catch (e) {
// Fallback: create empty transcript if loading fails
return podcast_search.Transcript();
}
}
/// Parse HTML transcript content into a transcript object
Future<podcast_search.Transcript> _loadHtmlTranscript(TranscriptUrl transcriptUrl) async {
try {
final response = await http.get(Uri.parse(transcriptUrl.url));
if (response.statusCode != 200) {
return podcast_search.Transcript();
}
final document = html.parse(response.body);
final subtitles = <podcast_search.Subtitle>[];
// For HTML transcripts, find the main content area and render as a single block
String transcriptContent = '';
// Try to find the main transcript content area
final transcriptContainer = document.querySelector('.transcript, .content, main, article') ??
document.querySelector('body');
if (transcriptContainer != null) {
transcriptContent = transcriptContainer.innerHtml;
// Clean up common unwanted elements
final cleanDoc = html.parse(transcriptContent);
// Remove navigation, headers, footers, ads, etc.
for (final selector in ['nav', 'header', 'footer', '.nav', '.navigation', '.ads', '.advertisement', '.sidebar']) {
cleanDoc.querySelectorAll(selector).forEach((el) => el.remove());
}
transcriptContent = cleanDoc.body?.innerHtml ?? transcriptContent;
// Process markdown-style links [text](url) -> <a href="url">text</a>
transcriptContent = transcriptContent.replaceAllMapped(
RegExp(r'\[([^\]]+)\]\(([^)]+)\)'),
(match) => '<a href="${match.group(2)}">${match.group(1)}</a>',
);
// Create a single subtitle entry for the entire HTML transcript
subtitles.add(podcast_search.Subtitle(
index: 0,
start: const Duration(seconds: 0),
end: const Duration(seconds: 1), // Minimal duration since timing doesn't matter
data: '{{HTMLFULL}}$transcriptContent',
speaker: '',
));
}
return podcast_search.Transcript(subtitles: subtitles);
} catch (e) {
debugPrint('Error parsing HTML transcript: $e');
return podcast_search.Transcript();
}
}
static Future<podcast_search.SearchResult> _search(Map<String, String?> searchParams) {
var term = searchParams['term']!;
var provider = searchParams['searchProvider'] == 'itunes'
? const podcast_search.ITunesProvider()
: podcast_search.PodcastIndexProvider(
key: podcastIndexKey,
secret: podcastIndexSecret,
);
return podcast_search.Search(
userAgent: Environment.userAgent(),
searchProvider: provider,
).search(term).timeout(const Duration(seconds: 30));
}
static Future<podcast_search.SearchResult> _charts(Map<String, String?> searchParams) {
var provider = searchParams['searchProvider'] == 'itunes'
? const podcast_search.ITunesProvider()
: podcast_search.PodcastIndexProvider(
key: podcastIndexKey,
secret: podcastIndexSecret,
);
var countryCode = searchParams['countryCode'];
var languageCode = searchParams['languageCode'] ?? '';
var country = podcast_search.Country.none;
if (countryCode != null && countryCode.isNotEmpty) {
country = podcast_search.Country.values.where((element) => element.code == countryCode).first;
}
return podcast_search.Search(userAgent: Environment.userAgent(), searchProvider: provider)
.charts(genre: searchParams['genre']!, country: country, language: languageCode, limit: 50)
.timeout(const Duration(seconds: 30));
}
Future<podcast_search.Podcast> _loadFeed(String url) {
_setupSecurityContext();
// In podcast_search 0.7.11, use Feed.loadFeed or create a Feed instance
return podcast_search.Feed.loadFeed(url: url, userAgent: Environment.userAgent());
}
void _setupSecurityContext() {
if (_certificateAuthorityBytes.isNotEmpty && _defaultSecurityContext == null) {
SecurityContext.defaultContext.setTrustedCertificatesBytes(_certificateAuthorityBytes);
_defaultSecurityContext = SecurityContext.defaultContext;
}
}
@override
void addClientAuthorityBytes(List<int> certificateAuthorityBytes) {
_certificateAuthorityBytes = certificateAuthorityBytes;
}
}