Initial commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:smplayer/core/infra/emby_api/emby_headers.dart';
|
||||
|
||||
void main() {
|
||||
group('EmbyRequestHeaders.media', () {
|
||||
test('媒体流 UA 使用基础 UA', () {
|
||||
final headers = EmbyRequestHeaders.media(
|
||||
deviceId: '6f43c257-2d5e-407e-9827-0a59d37ceb97',
|
||||
deviceName: 'TestDevice',
|
||||
token: 't',
|
||||
userId: 'u',
|
||||
);
|
||||
|
||||
expect(headers['User-Agent'], EmbyRequestHeaders.userAgent);
|
||||
expect(headers['Accept'], '*/*');
|
||||
expect(headers['Accept-Language'], EmbyRequestHeaders.acceptLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
group('EmbyRequestHeaders.directMedia', () {
|
||||
test('播放器直连头不包含凭据或设备标识', () {
|
||||
final headers = EmbyRequestHeaders.directMedia();
|
||||
final normalizedNames = headers.keys
|
||||
.map((name) => name.toLowerCase())
|
||||
.toSet();
|
||||
|
||||
expect(headers['User-Agent'], EmbyRequestHeaders.userAgent);
|
||||
expect(headers['Accept'], '*/*');
|
||||
expect(normalizedNames, isNot(contains('authorization')));
|
||||
expect(normalizedNames, isNot(contains('cookie')));
|
||||
expect(normalizedNames, isNot(contains('x-emby-token')));
|
||||
expect(normalizedNames, isNot(contains('x-mediabrowser-token')));
|
||||
expect(normalizedNames, isNot(contains('x-emby-authorization')));
|
||||
expect(normalizedNames, isNot(contains('x-emby-device-id')));
|
||||
});
|
||||
});
|
||||
|
||||
group('SenPlayer 身份对齐', () {
|
||||
test('UA 为 SenPlayer 固定值', () {
|
||||
expect(EmbyRequestHeaders.userAgent, 'SenPlayer/6.2.0');
|
||||
expect(EmbyRequestHeaders.clientName, 'SenPlayer');
|
||||
expect(EmbyRequestHeaders.version, '6.1.0');
|
||||
});
|
||||
|
||||
test('auth 头字面格式与 SenPlayer 身份一致', () {
|
||||
final auth = EmbyAuthHeader.build(
|
||||
deviceId: 'd',
|
||||
deviceName: 'Mac',
|
||||
token: 't',
|
||||
userId: 'u',
|
||||
);
|
||||
expect(
|
||||
auth,
|
||||
'MediaBrowser Token="t", Emby UserId="u", Client="SenPlayer", '
|
||||
'Device="Mac", DeviceId="d", Version="6.1.0"',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:smplayer/core/contracts/library.dart';
|
||||
import 'package:smplayer/core/contracts/playback.dart';
|
||||
import 'package:smplayer/core/domain/ports/emby_gateway.dart';
|
||||
import 'package:smplayer/core/infra/emby_api/playback_resolver.dart';
|
||||
|
||||
|
||||
class _UnusedEmbyGateway implements EmbyGateway {
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
const ctx = AuthedRequestContext(
|
||||
baseUrl: 'https://emby.example.com',
|
||||
token: 'tok123',
|
||||
userId: 'user1',
|
||||
);
|
||||
|
||||
final resolver = PlaybackResolver(
|
||||
gateway: _UnusedEmbyGateway(),
|
||||
deviceId: 'device-1',
|
||||
deviceName: 'TestBox',
|
||||
);
|
||||
|
||||
EmbyRawItem item({int? resumeTicks}) => EmbyRawItem(
|
||||
Id: 'item1',
|
||||
Name: 'Movie One',
|
||||
Type: 'Movie',
|
||||
RunTimeTicks: 600 * 10000000,
|
||||
UserData: resumeTicks == null
|
||||
? null
|
||||
: EmbyRawUserData(PlaybackPositionTicks: resumeTicks),
|
||||
);
|
||||
|
||||
Map<String, dynamic> playbackInfo(Map<String, dynamic> source) => {
|
||||
'PlaySessionId': 'ps1',
|
||||
'MediaSources': [source],
|
||||
};
|
||||
|
||||
group('PlaybackResolver.prepare 流地址优先级', () {
|
||||
test('DirectStreamUrl 优先,playMethod=DirectStream', () async {
|
||||
final result = await resolver.prepare(
|
||||
ctx: ctx,
|
||||
payload: const PlaybackRequestPayload(itemId: 'item1'),
|
||||
preloadedItem: item(),
|
||||
preloadedPlaybackInfo: playbackInfo({
|
||||
'Id': 's1',
|
||||
'Container': 'mkv',
|
||||
'DirectStreamUrl': '/Videos/item1/stream.mkv?x=1',
|
||||
'TranscodingUrl': '/Videos/item1/main.m3u8',
|
||||
'SupportsDirectStream': true,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
result.streamUrl,
|
||||
'https://emby.example.com/Videos/item1/stream.mkv?x=1&api_key=tok123',
|
||||
);
|
||||
expect(result.playMethod, 'DirectStream');
|
||||
expect(result.directStream, isTrue);
|
||||
});
|
||||
|
||||
test('无 DirectStreamUrl 时用 TranscodingUrl,playMethod=Transcode', () async {
|
||||
final result = await resolver.prepare(
|
||||
ctx: ctx,
|
||||
payload: const PlaybackRequestPayload(itemId: 'item1'),
|
||||
preloadedItem: item(),
|
||||
preloadedPlaybackInfo: playbackInfo({
|
||||
'Id': 's1',
|
||||
'Container': 'mkv',
|
||||
'TranscodingUrl': '/Videos/item1/main.m3u8',
|
||||
}),
|
||||
);
|
||||
expect(result.streamUrl, contains('/Videos/item1/main.m3u8'));
|
||||
expect(result.streamUrl, contains('api_key=tok123'));
|
||||
expect(result.playMethod, 'Transcode');
|
||||
});
|
||||
|
||||
test('跨域绝对 CDN 地址不追加 Emby token', () async {
|
||||
final result = await resolver.prepare(
|
||||
ctx: ctx,
|
||||
payload: const PlaybackRequestPayload(itemId: 'item1'),
|
||||
preloadedItem: item(),
|
||||
preloadedPlaybackInfo: playbackInfo({
|
||||
'Id': 's1',
|
||||
'Container': 'mkv',
|
||||
'DirectStreamUrl':
|
||||
'https://cdn.example.net/media/movie.mkv?signature=signed',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
result.streamUrl,
|
||||
'https://cdn.example.net/media/movie.mkv?signature=signed',
|
||||
);
|
||||
expect(result.streamUrl, isNot(contains('tok123')));
|
||||
expect(result.streamUrl, isNot(contains('api_key')));
|
||||
});
|
||||
|
||||
test('部署在子路径下时保留 Emby base path', () async {
|
||||
const subpathContext = AuthedRequestContext(
|
||||
baseUrl: 'https://emby.example.com/emby',
|
||||
token: 'tok123',
|
||||
userId: 'user1',
|
||||
);
|
||||
final result = await resolver.prepare(
|
||||
ctx: subpathContext,
|
||||
payload: const PlaybackRequestPayload(itemId: 'item1'),
|
||||
preloadedItem: item(),
|
||||
preloadedPlaybackInfo: playbackInfo({
|
||||
'Id': 's1',
|
||||
'Container': 'mkv',
|
||||
'DirectStreamUrl': '/Videos/item1/stream.mkv',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
result.streamUrl,
|
||||
'https://emby.example.com/emby/Videos/item1/stream.mkv?api_key=tok123',
|
||||
);
|
||||
});
|
||||
|
||||
test('两者皆无时回退静态流(含容器扩展名与鉴权参数)', () async {
|
||||
final result = await resolver.prepare(
|
||||
ctx: ctx,
|
||||
payload: const PlaybackRequestPayload(itemId: 'item1'),
|
||||
preloadedItem: item(),
|
||||
preloadedPlaybackInfo: playbackInfo({'Id': 's1', 'Container': 'mp4'}),
|
||||
);
|
||||
expect(
|
||||
result.streamUrl,
|
||||
startsWith('https://emby.example.com/Videos/item1/stream.mp4?'),
|
||||
);
|
||||
expect(result.streamUrl, contains('api_key=tok123'));
|
||||
expect(result.streamUrl, contains('MediaSourceId=s1'));
|
||||
expect(result.streamUrl, contains('PlaySessionId=ps1'));
|
||||
expect(result.playMethod, 'DirectStream');
|
||||
});
|
||||
|
||||
test('按 mediaSourceId 选源,未命中回退首源', () async {
|
||||
final info = {
|
||||
'MediaSources': [
|
||||
{'Id': 's1', 'DirectStreamUrl': '/Videos/item1/a.mkv'},
|
||||
{'Id': 's2', 'DirectStreamUrl': '/Videos/item1/b.mkv'},
|
||||
],
|
||||
};
|
||||
final picked = await resolver.prepare(
|
||||
ctx: ctx,
|
||||
payload: const PlaybackRequestPayload(
|
||||
itemId: 'item1',
|
||||
mediaSourceId: 's2',
|
||||
),
|
||||
preloadedItem: item(),
|
||||
preloadedPlaybackInfo: info,
|
||||
);
|
||||
expect(picked.streamUrl, contains('/Videos/item1/b.mkv'));
|
||||
|
||||
final fallback = await resolver.prepare(
|
||||
ctx: ctx,
|
||||
payload: const PlaybackRequestPayload(
|
||||
itemId: 'item1',
|
||||
mediaSourceId: 'missing',
|
||||
),
|
||||
preloadedItem: item(),
|
||||
preloadedPlaybackInfo: info,
|
||||
);
|
||||
expect(fallback.streamUrl, contains('/Videos/item1/a.mkv'));
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaybackResolver.prepare 续播位置', () {
|
||||
Future<double> startSecondsFor(PlaybackRequestPayload payload) async {
|
||||
final result = await resolver.prepare(
|
||||
ctx: ctx,
|
||||
payload: payload,
|
||||
preloadedItem: item(resumeTicks: 120 * 10000000),
|
||||
preloadedPlaybackInfo: playbackInfo({
|
||||
'Id': 's1',
|
||||
'DirectStreamUrl': '/Videos/item1/a.mkv',
|
||||
}),
|
||||
);
|
||||
return result.startPositionSeconds;
|
||||
}
|
||||
|
||||
test('playFromStart 强制从头', () async {
|
||||
expect(
|
||||
await startSecondsFor(
|
||||
const PlaybackRequestPayload(itemId: 'item1', playFromStart: true),
|
||||
),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('显式 startPositionTicks 优先于 UserData', () async {
|
||||
expect(
|
||||
await startSecondsFor(
|
||||
const PlaybackRequestPayload(
|
||||
itemId: 'item1',
|
||||
startPositionTicks: 60 * 10000000,
|
||||
),
|
||||
),
|
||||
60,
|
||||
);
|
||||
});
|
||||
|
||||
test('默认取 UserData.PlaybackPositionTicks', () async {
|
||||
expect(
|
||||
await startSecondsFor(const PlaybackRequestPayload(itemId: 'item1')),
|
||||
120,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaybackResolver.resolveSubtitles', () {
|
||||
test('未提供 baseUrl/token 时外挂轨全部过滤,仅留内封(详尽用例见 '
|
||||
'playback_resolver_subtitles_test.dart)', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(const [
|
||||
EmbyRawMediaStream(Index: 1, Type: 'Subtitle', Title: 'embedded'),
|
||||
EmbyRawMediaStream(
|
||||
Index: 2,
|
||||
Type: 'Subtitle',
|
||||
Codec: 'srt',
|
||||
IsExternal: true,
|
||||
),
|
||||
EmbyRawMediaStream(
|
||||
Index: 3,
|
||||
Type: 'Subtitle',
|
||||
Codec: 'srt',
|
||||
DeliveryMethod: 'External',
|
||||
),
|
||||
EmbyRawMediaStream(
|
||||
Index: 4,
|
||||
Type: 'Subtitle',
|
||||
Codec: 'srt',
|
||||
DeliveryUrl: '/subs/4.srt',
|
||||
),
|
||||
EmbyRawMediaStream(Index: 5, Type: 'Audio'),
|
||||
]);
|
||||
expect(subtitles, hasLength(1));
|
||||
expect(subtitles.single.index, 1);
|
||||
expect(subtitles.single.title, 'embedded');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:smplayer/core/contracts/library.dart';
|
||||
import 'package:smplayer/core/infra/emby_api/playback_resolver.dart';
|
||||
|
||||
void main() {
|
||||
const baseUrl = 'https://emby.example.com';
|
||||
const token = 'tok123';
|
||||
const itemId = 'item1';
|
||||
const mediaSourceId = 'src9';
|
||||
|
||||
EmbyRawMediaStream embeddedSubtitle({
|
||||
int index = 2,
|
||||
String codec = 'subrip',
|
||||
String? title,
|
||||
String? language,
|
||||
bool isDefault = false,
|
||||
bool isForced = false,
|
||||
}) => EmbyRawMediaStream(
|
||||
Index: index,
|
||||
Type: 'Subtitle',
|
||||
Codec: codec,
|
||||
Title: title,
|
||||
Language: language,
|
||||
IsDefault: isDefault,
|
||||
IsForced: isForced,
|
||||
);
|
||||
|
||||
EmbyRawMediaStream externalSubtitle({
|
||||
int index = 10,
|
||||
String codec = 'srt',
|
||||
String? deliveryUrl = '/Videos/item1/10/Subtitles/stream.srt',
|
||||
String? deliveryMethod = 'External',
|
||||
bool isExternal = true,
|
||||
String? title,
|
||||
String? language,
|
||||
}) => EmbyRawMediaStream(
|
||||
Index: index,
|
||||
Type: 'Subtitle',
|
||||
Codec: codec,
|
||||
IsExternal: isExternal,
|
||||
DeliveryMethod: deliveryMethod,
|
||||
DeliveryUrl: deliveryUrl,
|
||||
Title: title,
|
||||
Language: language,
|
||||
);
|
||||
|
||||
group('PlaybackResolver.resolveSubtitles 外挂字幕', () {
|
||||
test('内封轨保留且 isExternal=false(未传 itemId 时无抽取 URL)', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[embeddedSubtitle(index: 2, title: '简中', language: 'chi')],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(subtitles, hasLength(1));
|
||||
expect(subtitles.single.index, 2);
|
||||
expect(subtitles.single.isExternal, isFalse);
|
||||
expect(subtitles.single.deliveryUrl, isNull);
|
||||
expect(subtitles.single.codec, 'subrip');
|
||||
});
|
||||
|
||||
test('外挂文本轨保留,deliveryUrl 补全 base 与 api_key', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[externalSubtitle(index: 10, codec: 'srt', language: 'chi')],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(subtitles, hasLength(1));
|
||||
final track = subtitles.single;
|
||||
expect(track.isExternal, isTrue);
|
||||
expect(
|
||||
track.deliveryUrl,
|
||||
'$baseUrl/Videos/item1/10/Subtitles/stream.srt?api_key=$token',
|
||||
);
|
||||
});
|
||||
|
||||
test('deliveryUrl 已是绝对地址时不重复补 base,只补 api_key', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[
|
||||
externalSubtitle(
|
||||
deliveryUrl: 'https://cdn.example.com/subs/a.ass',
|
||||
codec: 'ass',
|
||||
),
|
||||
],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(
|
||||
subtitles.single.deliveryUrl,
|
||||
'https://cdn.example.com/subs/a.ass?api_key=$token',
|
||||
);
|
||||
});
|
||||
|
||||
test('外挂位图轨(pgs/vobsub/dvdsub)被过滤', () {
|
||||
for (final bitmapCodec in ['pgssub', 'pgs', 'dvdsub', 'sup', 'vobsub']) {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[externalSubtitle(codec: bitmapCodec)],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(subtitles, isEmpty, reason: 'codec=$bitmapCodec 应被过滤');
|
||||
}
|
||||
});
|
||||
|
||||
test('内封位图轨保留(由引擎渲染,行为不变)', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[embeddedSubtitle(codec: 'pgssub')],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(subtitles, hasLength(1));
|
||||
expect(subtitles.single.isExternal, isFalse);
|
||||
});
|
||||
|
||||
test('外挂轨缺 DeliveryUrl 时被过滤', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[externalSubtitle(deliveryUrl: null)],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(subtitles, isEmpty);
|
||||
});
|
||||
|
||||
test('未提供 baseUrl/token 时外挂轨被过滤(内封不受影响)', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles([
|
||||
embeddedSubtitle(index: 2),
|
||||
externalSubtitle(index: 10),
|
||||
]);
|
||||
expect(subtitles, hasLength(1));
|
||||
expect(subtitles.single.index, 2);
|
||||
});
|
||||
|
||||
test('仅 DeliveryMethod==External(无 IsExternal 标记)也按外挂处理', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[externalSubtitle(isExternal: false, deliveryMethod: 'External')],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(subtitles.single.isExternal, isTrue);
|
||||
});
|
||||
|
||||
test('混合列表保持原顺序,各归其类', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[
|
||||
embeddedSubtitle(index: 2, codec: 'ass'),
|
||||
externalSubtitle(index: 10, codec: 'srt'),
|
||||
externalSubtitle(index: 11, codec: 'pgssub'),
|
||||
embeddedSubtitle(index: 3, codec: 'subrip'),
|
||||
],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
);
|
||||
expect(subtitles.map((t) => t.index).toList(), [2, 10, 3]);
|
||||
expect(subtitles.map((t) => t.isExternal).toList(), [false, true, false]);
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaybackResolver.resolveSubtitles 内封文本抽取地址', () {
|
||||
test('内封文本轨生成服务端抽取 deliveryUrl(isExternal 仍为 false)', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[embeddedSubtitle(index: 2, codec: 'subrip', language: 'chi')],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
itemId: itemId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
);
|
||||
expect(subtitles, hasLength(1));
|
||||
final track = subtitles.single;
|
||||
expect(track.isExternal, isFalse);
|
||||
|
||||
|
||||
expect(
|
||||
track.deliveryUrl,
|
||||
'$baseUrl/Videos/$itemId/$mediaSourceId/Subtitles/2/0/Stream.subrip'
|
||||
'?api_key=$token',
|
||||
);
|
||||
});
|
||||
|
||||
test('扩展名用 codec 原名 passthrough(webvtt 归一化为 vtt)', () {
|
||||
final cases = {'subrip': 'subrip', 'ssa': 'ssa', 'webvtt': 'vtt'};
|
||||
cases.forEach((codec, ext) {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[embeddedSubtitle(index: 4, codec: codec)],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
itemId: itemId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
);
|
||||
expect(
|
||||
subtitles.single.deliveryUrl,
|
||||
'$baseUrl/Videos/$itemId/$mediaSourceId/Subtitles/4/0/Stream.$ext'
|
||||
'?api_key=$token',
|
||||
reason: 'codec=$codec 应映射为 .$ext',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('srt/ass/vtt codec 直接用作扩展名,含 /0/ 段', () {
|
||||
final cases = {'srt': 'srt', 'ass': 'ass', 'vtt': 'vtt'};
|
||||
cases.forEach((codec, ext) {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[embeddedSubtitle(index: 5, codec: codec)],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
itemId: itemId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
);
|
||||
expect(
|
||||
subtitles.single.deliveryUrl,
|
||||
endsWith('/Subtitles/5/0/Stream.$ext?api_key=$token'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('内封位图轨不生成抽取 URL(deliveryUrl 为 null)', () {
|
||||
for (final bitmapCodec in ['pgssub', 'pgs', 'dvdsub', 'vobsub', 'sup']) {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[embeddedSubtitle(index: 6, codec: bitmapCodec)],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
itemId: itemId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
);
|
||||
expect(subtitles, hasLength(1));
|
||||
expect(
|
||||
subtitles.single.deliveryUrl,
|
||||
isNull,
|
||||
reason: 'codec=$bitmapCodec 不应生成抽取 URL',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('缺 mediaSourceId 时内封文本轨不生成抽取 URL', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[embeddedSubtitle(index: 2, codec: 'subrip')],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
itemId: itemId,
|
||||
);
|
||||
expect(subtitles.single.deliveryUrl, isNull);
|
||||
});
|
||||
|
||||
test('外挂轨不受内封抽取逻辑影响,仍用自身 DeliveryUrl', () {
|
||||
final subtitles = PlaybackResolver.resolveSubtitles(
|
||||
[
|
||||
embeddedSubtitle(index: 2, codec: 'subrip'),
|
||||
externalSubtitle(index: 10, codec: 'srt'),
|
||||
],
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
itemId: itemId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
);
|
||||
final embedded = subtitles.firstWhere((t) => !t.isExternal);
|
||||
final external = subtitles.firstWhere((t) => t.isExternal);
|
||||
expect(
|
||||
embedded.deliveryUrl,
|
||||
'$baseUrl/Videos/$itemId/$mediaSourceId/Subtitles/2/0/Stream.subrip'
|
||||
'?api_key=$token',
|
||||
);
|
||||
expect(
|
||||
external.deliveryUrl,
|
||||
'$baseUrl/Videos/item1/10/Subtitles/stream.srt?api_key=$token',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaybackResolver.isTextSubtitleCodec', () {
|
||||
test('文本 codec 判定大小写不敏感', () {
|
||||
for (final textCodec in [
|
||||
'srt',
|
||||
'SUBRIP',
|
||||
'vtt',
|
||||
'WebVTT',
|
||||
'ass',
|
||||
'ssa',
|
||||
]) {
|
||||
expect(
|
||||
PlaybackResolver.isTextSubtitleCodec(textCodec),
|
||||
isTrue,
|
||||
reason: 'codec=$textCodec',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('位图/未知/null codec 返回 false', () {
|
||||
for (final nonText in ['pgssub', 'dvdsub', 'sup', 'unknown', null]) {
|
||||
expect(
|
||||
PlaybackResolver.isTextSubtitleCodec(nonText),
|
||||
isFalse,
|
||||
reason: 'codec=$nonText',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user