feat(app): 完善播放协议搜索与空状态
This commit is contained in:
1 parent
671e0ce143
commit
ef29fc5f15
85 files changed
+2976
-768
No files matched your search
@@ -14,6 +14,7 @@ class PlayTicket {
|
||||
this.unlockCoin = 0,
|
||||
this.previewSeconds = 0,
|
||||
this.qualities = const [],
|
||||
this.tracks = const [],
|
||||
this.thumbnailsUrl,
|
||||
this.likeCount,
|
||||
this.commentCount,
|
||||
@@ -39,8 +40,11 @@ class PlayTicket {
|
||||
/// 试看秒数:仅 `COIN_REQUIRED` 时有意义(付费解锁在 B2 做)。
|
||||
final int previewSeconds;
|
||||
|
||||
/// 后端下发的清晰度档位(如 AUTO / 720P / 1080P),HLS 由播放器自动切换。
|
||||
/// 后端下发的清晰度档位(如 AUTO / 720p / 1080p)。
|
||||
final List<String> qualities;
|
||||
|
||||
/// 各档签名地址。AUTO 是主列表,点选其它档位时换成对应地址。
|
||||
final List<PlayQualityTrack> tracks;
|
||||
final String? thumbnailsUrl;
|
||||
final int? likeCount;
|
||||
final int? commentCount;
|
||||
@@ -64,6 +68,7 @@ class PlayTicket {
|
||||
.whereType<String>()
|
||||
.where((quality) => quality.isNotEmpty)
|
||||
.toList(),
|
||||
tracks: _tracks(json['tracks'], json['playUrl'] as String? ?? ''),
|
||||
thumbnailsUrl: _optionalString(json['thumbnailsUrl']),
|
||||
likeCount: json['likeCount'] == null
|
||||
? null
|
||||
@@ -75,6 +80,67 @@ class PlayTicket {
|
||||
}
|
||||
}
|
||||
|
||||
List<PlayQualityTrack> _tracks(dynamic raw, String playUrl) {
|
||||
final tracks = <PlayQualityTrack>[];
|
||||
if (raw is List) {
|
||||
for (final item in raw) {
|
||||
if (item is! Map) continue;
|
||||
final quality = item['quality'] as String? ?? '';
|
||||
final url = item['playUrl'] as String? ?? '';
|
||||
if (quality.isEmpty || url.isEmpty) continue;
|
||||
tracks.add(PlayQualityTrack(quality: quality, playUrl: url));
|
||||
}
|
||||
}
|
||||
if (tracks.isEmpty && playUrl.isNotEmpty) {
|
||||
tracks.add(PlayQualityTrack(quality: 'AUTO', playUrl: playUrl));
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/// 按票据里的分轨地址取播放地址;没有分轨时,从主列表路径推出同目录的单档地址。
|
||||
String? qualityPlayUrl({
|
||||
required String quality,
|
||||
required List<PlayQualityTrack> tracks,
|
||||
required String currentUrl,
|
||||
}) {
|
||||
final wanted = quality.toUpperCase();
|
||||
for (final track in tracks) {
|
||||
if (track.quality.toUpperCase() == wanted && track.playUrl.isNotEmpty) {
|
||||
return track.playUrl;
|
||||
}
|
||||
}
|
||||
return siblingRenditionUrl(currentUrl, wanted);
|
||||
}
|
||||
|
||||
/// `.../master.m3u8` 与 `.../720p/index.m3u8` 互相换算,查询参数原样保留。
|
||||
String? siblingRenditionUrl(String currentUrl, String quality) {
|
||||
final uri = Uri.tryParse(currentUrl);
|
||||
if (uri == null || uri.path.isEmpty) return null;
|
||||
final path = uri.path;
|
||||
const masterSuffix = '/master.m3u8';
|
||||
String? root;
|
||||
if (path.endsWith(masterSuffix)) {
|
||||
root = path.substring(0, path.length - masterSuffix.length);
|
||||
} else {
|
||||
final match = RegExp(r'/(\d+p)/index\.m3u8$').firstMatch(path);
|
||||
if (match == null) return null;
|
||||
root = path.substring(0, match.start);
|
||||
}
|
||||
final next = quality.toUpperCase() == 'AUTO'
|
||||
? '$root$masterSuffix'
|
||||
: '$root/${quality.toLowerCase()}/index.m3u8';
|
||||
if (next == path) return currentUrl;
|
||||
return uri.replace(path: next).toString();
|
||||
}
|
||||
|
||||
/// 一档清晰度的签名播放地址。
|
||||
class PlayQualityTrack {
|
||||
const PlayQualityTrack({required this.quality, required this.playUrl});
|
||||
|
||||
final String quality;
|
||||
final String playUrl;
|
||||
}
|
||||
|
||||
String? _optionalString(dynamic value) {
|
||||
final text = value is String ? value.trim() : '';
|
||||
return text.isEmpty ? null : text;
|
||||
|
||||
@@ -18,6 +18,7 @@ class WatchProgress {
|
||||
this.totalEpisodes,
|
||||
this.finished = false,
|
||||
this.playCount = 0,
|
||||
this.viewCount,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
@@ -31,6 +32,9 @@ class WatchProgress {
|
||||
final Duration duration;
|
||||
final bool finished;
|
||||
final int playCount;
|
||||
|
||||
/// 短剧热度。历史列表没下发时为空,不能拿用户自己的 playCount 冒充。
|
||||
final int? viewCount;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
bool get hasPosition => position > Duration.zero;
|
||||
@@ -57,6 +61,9 @@ class WatchProgress {
|
||||
duration: Duration(milliseconds: jsonToInt(json['durationMs'])),
|
||||
finished: json['finished'] == true,
|
||||
playCount: jsonToInt(json['playCount']),
|
||||
viewCount: json['viewCount'] == null
|
||||
? null
|
||||
: jsonToInt(json['viewCount']),
|
||||
updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''),
|
||||
);
|
||||
}
|
||||
@@ -72,6 +79,7 @@ class WatchProgress {
|
||||
'durationMs': duration.inMilliseconds,
|
||||
'finished': finished,
|
||||
'playCount': playCount,
|
||||
if (viewCount != null) 'viewCount': viewCount,
|
||||
if (updatedAt != null) 'updatedAt': updatedAt!.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user