added cargo files
This commit is contained in:
420
PinePods-0.8.2/mobile/lib/entities/episode.dart
Normal file
420
PinePods-0.8.2/mobile/lib/entities/episode.dart
Normal file
@@ -0,0 +1,420 @@
|
||||
// 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 'package:pinepods_mobile/core/annotations.dart';
|
||||
import 'package:pinepods_mobile/core/extensions.dart';
|
||||
import 'package:pinepods_mobile/entities/chapter.dart';
|
||||
import 'package:pinepods_mobile/entities/downloadable.dart';
|
||||
import 'package:pinepods_mobile/entities/person.dart';
|
||||
import 'package:pinepods_mobile/entities/transcript.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:html/parser.dart' show parseFragment;
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// An object that represents an individual episode of a Podcast.
|
||||
///
|
||||
/// An Episode can be used in conjunction with a [Downloadable] to
|
||||
/// determine if the Episode is available on the local filesystem.
|
||||
class Episode {
|
||||
final log = Logger('Episode');
|
||||
|
||||
/// Database ID
|
||||
int? id;
|
||||
|
||||
/// A String GUID for the episode.
|
||||
final String guid;
|
||||
|
||||
/// The GUID for an associated podcast. If an episode has been downloaded
|
||||
/// without subscribing to a podcast this may be null.
|
||||
String? pguid;
|
||||
|
||||
/// If the episode is currently being downloaded, this contains the unique
|
||||
/// ID supplied by the download manager for the episode.
|
||||
String? downloadTaskId;
|
||||
|
||||
/// The path to the directory containing the download for this episode; or null.
|
||||
String? filepath;
|
||||
|
||||
/// The filename of the downloaded episode; or null.
|
||||
String? filename;
|
||||
|
||||
/// The current downloading state of the episode.
|
||||
DownloadState downloadState = DownloadState.none;
|
||||
|
||||
/// The name of the podcast the episode is part of.
|
||||
String? podcast;
|
||||
|
||||
/// The episode title.
|
||||
String? title;
|
||||
|
||||
/// The episode description. This could be plain text or HTML.
|
||||
String? description;
|
||||
|
||||
/// More detailed description - optional.
|
||||
String? content;
|
||||
|
||||
/// External link
|
||||
String? link;
|
||||
|
||||
/// URL to the episode artwork image.
|
||||
String? imageUrl;
|
||||
|
||||
/// URL to a thumbnail version of the episode artwork image.
|
||||
String? thumbImageUrl;
|
||||
|
||||
/// The date the episode was published (if known).
|
||||
DateTime? publicationDate;
|
||||
|
||||
/// The URL for the episode location.
|
||||
String? contentUrl;
|
||||
|
||||
/// Author of the episode if known.
|
||||
String? author;
|
||||
|
||||
/// The season the episode is part of if available.
|
||||
int season;
|
||||
|
||||
/// The episode number within a season if available.
|
||||
int episode;
|
||||
|
||||
/// The duration of the episode in milliseconds. This can be populated either from
|
||||
/// the RSS if available, or determined from the MP3 file at stream/download time.
|
||||
int duration;
|
||||
|
||||
/// Stores the current position within the episode in milliseconds. Used for resuming.
|
||||
int position;
|
||||
|
||||
/// Stores the progress of the current download progress if available.
|
||||
int? downloadPercentage;
|
||||
|
||||
/// True if this episode is 'marked as played'.
|
||||
bool played;
|
||||
|
||||
/// URL pointing to a JSON file containing chapter information if available.
|
||||
String? chaptersUrl;
|
||||
|
||||
/// List of chapters for the episode if available.
|
||||
List<Chapter> chapters;
|
||||
|
||||
/// List of transcript URLs for the episode if available.
|
||||
List<TranscriptUrl> transcriptUrls;
|
||||
|
||||
List<Person> persons;
|
||||
|
||||
/// Currently downloaded or in use transcript for the episode.To minimise memory
|
||||
/// use, this is cleared when an episode download is deleted, or a streamed episode stopped.
|
||||
Transcript? transcript;
|
||||
|
||||
/// Link to a currently stored transcript for this episode.
|
||||
int? transcriptId;
|
||||
|
||||
/// Date and time episode was last updated and persisted.
|
||||
DateTime? lastUpdated;
|
||||
|
||||
/// Processed version of episode description.
|
||||
String? _descriptionText;
|
||||
|
||||
/// Index of the currently playing chapter it available. Transient.
|
||||
int? chapterIndex;
|
||||
|
||||
/// Current chapter we are listening to if this episode has chapters. Transient.
|
||||
Chapter? currentChapter;
|
||||
|
||||
/// Set to true if chapter data is currently being loaded.
|
||||
@Transient()
|
||||
bool chaptersLoading = false;
|
||||
|
||||
@Transient()
|
||||
bool highlight = false;
|
||||
|
||||
@Transient()
|
||||
bool queued = false;
|
||||
|
||||
@Transient()
|
||||
bool streaming = true;
|
||||
|
||||
Episode({
|
||||
required this.guid,
|
||||
this.pguid,
|
||||
required this.podcast,
|
||||
this.id,
|
||||
this.downloadTaskId,
|
||||
this.filepath,
|
||||
this.filename,
|
||||
this.downloadState = DownloadState.none,
|
||||
this.title,
|
||||
this.description,
|
||||
this.content,
|
||||
this.link,
|
||||
String? imageUrl,
|
||||
String? thumbImageUrl,
|
||||
this.publicationDate,
|
||||
String? contentUrl,
|
||||
this.author,
|
||||
this.season = 0,
|
||||
this.episode = 0,
|
||||
this.duration = 0,
|
||||
this.position = 0,
|
||||
this.downloadPercentage = 0,
|
||||
this.played = false,
|
||||
this.highlight = false,
|
||||
String? chaptersUrl,
|
||||
this.chapters = const <Chapter>[],
|
||||
this.transcriptUrls = const <TranscriptUrl>[],
|
||||
this.persons = const <Person>[],
|
||||
this.transcriptId = 0,
|
||||
this.lastUpdated,
|
||||
}) : imageUrl = imageUrl?.forceHttps,
|
||||
thumbImageUrl = thumbImageUrl?.forceHttps,
|
||||
contentUrl = contentUrl?.forceHttps,
|
||||
chaptersUrl = chaptersUrl?.forceHttps;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'guid': guid,
|
||||
'pguid': pguid,
|
||||
'downloadTaskId': downloadTaskId,
|
||||
'filepath': filepath,
|
||||
'filename': filename,
|
||||
'downloadState': downloadState.index,
|
||||
'podcast': podcast,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'content': content,
|
||||
'link': link,
|
||||
'imageUrl': imageUrl,
|
||||
'thumbImageUrl': thumbImageUrl,
|
||||
'publicationDate': publicationDate?.millisecondsSinceEpoch.toString(),
|
||||
'contentUrl': contentUrl,
|
||||
'author': author,
|
||||
'season': season.toString(),
|
||||
'episode': episode.toString(),
|
||||
'duration': duration.toString(),
|
||||
'position': position.toString(),
|
||||
'downloadPercentage': downloadPercentage.toString(),
|
||||
'played': played ? 'true' : 'false',
|
||||
'chaptersUrl': chaptersUrl,
|
||||
'chapters': (chapters).map((chapter) => chapter.toMap()).toList(growable: false),
|
||||
'tid': transcriptId ?? 0,
|
||||
'transcriptUrls': (transcriptUrls).map((tu) => tu.toMap()).toList(growable: false),
|
||||
'persons': (persons).map((person) => person.toMap()).toList(growable: false),
|
||||
'lastUpdated': lastUpdated?.millisecondsSinceEpoch.toString() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
static Episode fromMap(int? key, Map<String, dynamic> episode) {
|
||||
var chapters = <Chapter>[];
|
||||
var transcriptUrls = <TranscriptUrl>[];
|
||||
var persons = <Person>[];
|
||||
|
||||
// We need to perform an 'is' on each loop to prevent Dart
|
||||
// from complaining that we have not set the type for chapter.
|
||||
if (episode['chapters'] != null) {
|
||||
for (var chapter in (episode['chapters'] as List)) {
|
||||
if (chapter is Map<String, dynamic>) {
|
||||
chapters.add(Chapter.fromMap(chapter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (episode['transcriptUrls'] != null) {
|
||||
for (var transcriptUrl in (episode['transcriptUrls'] as List)) {
|
||||
if (transcriptUrl is Map<String, dynamic>) {
|
||||
transcriptUrls.add(TranscriptUrl.fromMap(transcriptUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (episode['persons'] != null) {
|
||||
for (var person in (episode['persons'] as List)) {
|
||||
if (person is Map<String, dynamic>) {
|
||||
persons.add(Person.fromMap(person));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Episode(
|
||||
id: key,
|
||||
guid: episode['guid'] as String,
|
||||
pguid: episode['pguid'] as String?,
|
||||
downloadTaskId: episode['downloadTaskId'] as String?,
|
||||
filepath: episode['filepath'] as String?,
|
||||
filename: episode['filename'] as String?,
|
||||
downloadState: _determineState(episode['downloadState'] as int?),
|
||||
podcast: episode['podcast'] as String?,
|
||||
title: episode['title'] as String?,
|
||||
description: episode['description'] as String?,
|
||||
content: episode['content'] as String?,
|
||||
link: episode['link'] as String?,
|
||||
imageUrl: episode['imageUrl'] as String?,
|
||||
thumbImageUrl: episode['thumbImageUrl'] as String?,
|
||||
publicationDate: episode['publicationDate'] == null || episode['publicationDate'] == 'null'
|
||||
? DateTime.now()
|
||||
: DateTime.fromMillisecondsSinceEpoch(int.parse(episode['publicationDate'] as String)),
|
||||
contentUrl: episode['contentUrl'] as String?,
|
||||
author: episode['author'] as String?,
|
||||
season: int.parse(episode['season'] as String? ?? '0'),
|
||||
episode: int.parse(episode['episode'] as String? ?? '0'),
|
||||
duration: int.parse(episode['duration'] as String? ?? '0'),
|
||||
position: int.parse(episode['position'] as String? ?? '0'),
|
||||
downloadPercentage: int.parse(episode['downloadPercentage'] as String? ?? '0'),
|
||||
played: episode['played'] == 'true' ? true : false,
|
||||
chaptersUrl: episode['chaptersUrl'] as String?,
|
||||
chapters: chapters,
|
||||
transcriptUrls: transcriptUrls,
|
||||
persons: persons,
|
||||
transcriptId: episode['tid'] == null ? 0 : episode['tid'] as int?,
|
||||
lastUpdated: episode['lastUpdated'] == null || episode['lastUpdated'] == 'null'
|
||||
? DateTime.now()
|
||||
: DateTime.fromMillisecondsSinceEpoch(int.parse(episode['lastUpdated'] as String)),
|
||||
);
|
||||
}
|
||||
|
||||
static DownloadState _determineState(int? index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return DownloadState.none;
|
||||
case 1:
|
||||
return DownloadState.queued;
|
||||
case 2:
|
||||
return DownloadState.downloading;
|
||||
case 3:
|
||||
return DownloadState.failed;
|
||||
case 4:
|
||||
return DownloadState.cancelled;
|
||||
case 5:
|
||||
return DownloadState.paused;
|
||||
case 6:
|
||||
return DownloadState.downloaded;
|
||||
}
|
||||
|
||||
return DownloadState.none;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is Episode &&
|
||||
runtimeType == other.runtimeType &&
|
||||
guid == other.guid &&
|
||||
pguid == other.pguid &&
|
||||
downloadTaskId == other.downloadTaskId &&
|
||||
filepath == other.filepath &&
|
||||
filename == other.filename &&
|
||||
downloadState == other.downloadState &&
|
||||
podcast == other.podcast &&
|
||||
title == other.title &&
|
||||
description == other.description &&
|
||||
content == other.content &&
|
||||
link == other.link &&
|
||||
imageUrl == other.imageUrl &&
|
||||
thumbImageUrl == other.thumbImageUrl &&
|
||||
publicationDate?.millisecondsSinceEpoch == other.publicationDate?.millisecondsSinceEpoch &&
|
||||
contentUrl == other.contentUrl &&
|
||||
author == other.author &&
|
||||
season == other.season &&
|
||||
episode == other.episode &&
|
||||
duration == other.duration &&
|
||||
position == other.position &&
|
||||
downloadPercentage == other.downloadPercentage &&
|
||||
played == other.played &&
|
||||
chaptersUrl == other.chaptersUrl &&
|
||||
transcriptId == other.transcriptId &&
|
||||
listEquals(persons, other.persons) &&
|
||||
listEquals(chapters, other.chapters);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
guid.hashCode ^
|
||||
pguid.hashCode ^
|
||||
downloadTaskId.hashCode ^
|
||||
filepath.hashCode ^
|
||||
filename.hashCode ^
|
||||
downloadState.hashCode ^
|
||||
podcast.hashCode ^
|
||||
title.hashCode ^
|
||||
description.hashCode ^
|
||||
content.hashCode ^
|
||||
link.hashCode ^
|
||||
imageUrl.hashCode ^
|
||||
thumbImageUrl.hashCode ^
|
||||
publicationDate.hashCode ^
|
||||
contentUrl.hashCode ^
|
||||
author.hashCode ^
|
||||
season.hashCode ^
|
||||
episode.hashCode ^
|
||||
duration.hashCode ^
|
||||
position.hashCode ^
|
||||
downloadPercentage.hashCode ^
|
||||
played.hashCode ^
|
||||
chaptersUrl.hashCode ^
|
||||
chapters.hashCode ^
|
||||
transcriptId.hashCode ^
|
||||
lastUpdated.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Episode{id: $id, guid: $guid, pguid: $pguid, filepath: $filepath, title: $title, contentUrl: $contentUrl, episode: $episode, duration: $duration, position: $position, downloadPercentage: $downloadPercentage, played: $played, queued: $queued}';
|
||||
}
|
||||
|
||||
bool get downloaded => downloadPercentage == 100;
|
||||
|
||||
Duration get timeRemaining {
|
||||
if (position > 0 && duration > 0) {
|
||||
var currentPosition = Duration(milliseconds: position);
|
||||
|
||||
var tr = duration - currentPosition.inSeconds;
|
||||
|
||||
return Duration(seconds: tr);
|
||||
}
|
||||
|
||||
return const Duration(seconds: 0);
|
||||
}
|
||||
|
||||
double get percentagePlayed {
|
||||
if (position > 0 && duration > 0) {
|
||||
var pc = (position / (duration * 1000)) * 100;
|
||||
|
||||
if (pc > 100.0) {
|
||||
pc = 100.0;
|
||||
}
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
String? get descriptionText {
|
||||
if (_descriptionText == null || _descriptionText!.isEmpty) {
|
||||
if (description == null || description!.isEmpty) {
|
||||
_descriptionText = '';
|
||||
} else {
|
||||
// Replace break tags with space character for readability
|
||||
var formattedDescription = description!.replaceAll(RegExp(r'(<br/?>)+'), ' ');
|
||||
_descriptionText = parseFragment(formattedDescription).text;
|
||||
}
|
||||
}
|
||||
|
||||
return _descriptionText;
|
||||
}
|
||||
|
||||
bool get hasChapters => (chaptersUrl != null && chaptersUrl!.isNotEmpty) || chapters.isNotEmpty;
|
||||
|
||||
bool get hasTranscripts => transcriptUrls.isNotEmpty;
|
||||
|
||||
bool get chaptersAreLoaded => chaptersLoading == false && chapters.isNotEmpty;
|
||||
|
||||
bool get chaptersAreNotLoaded => chaptersLoading == true && chapters.isEmpty;
|
||||
|
||||
String? get positionalImageUrl {
|
||||
if (currentChapter != null && currentChapter!.imageUrl != null && currentChapter!.imageUrl!.isNotEmpty) {
|
||||
return currentChapter!.imageUrl;
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user