From 14773248d96d0bcb245000909eaae05619550ae9 Mon Sep 17 00:00:00 2001 From: nanxun Date: Sun, 17 May 2026 15:24:28 +0800 Subject: [PATCH] fix: track flutter mobile data layer --- .gitignore | 2 + .../data/models/live_recorder_models.dart | 1060 +++++++++++++++++ .../data/repositories/auth_repository.dart | 40 + .../repositories/live_rooms_repository.dart | 72 ++ .../data/repositories/logs_repository.dart | 34 + .../data/repositories/media_repository.dart | 39 + .../repositories/recordings_repository.dart | 82 ++ .../repositories/recovery_repository.dart | 29 + .../repositories/settings_repository.dart | 27 + 9 files changed, 1385 insertions(+) create mode 100644 mobile/lib/features/live_recorder/data/models/live_recorder_models.dart create mode 100644 mobile/lib/features/live_recorder/data/repositories/auth_repository.dart create mode 100644 mobile/lib/features/live_recorder/data/repositories/live_rooms_repository.dart create mode 100644 mobile/lib/features/live_recorder/data/repositories/logs_repository.dart create mode 100644 mobile/lib/features/live_recorder/data/repositories/media_repository.dart create mode 100644 mobile/lib/features/live_recorder/data/repositories/recordings_repository.dart create mode 100644 mobile/lib/features/live_recorder/data/repositories/recovery_repository.dart create mode 100644 mobile/lib/features/live_recorder/data/repositories/settings_repository.dart diff --git a/.gitignore b/.gitignore index 91ae0d3..b4649e6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ webapi-build.log webapi-build-no-restore.log artifacts/ data/ +!mobile/lib/features/live_recorder/data/ +!mobile/lib/features/live_recorder/data/** records/ docker-data/ src/LiveRecorder.WebApi/data/ diff --git a/mobile/lib/features/live_recorder/data/models/live_recorder_models.dart b/mobile/lib/features/live_recorder/data/models/live_recorder_models.dart new file mode 100644 index 0000000..7093a28 --- /dev/null +++ b/mobile/lib/features/live_recorder/data/models/live_recorder_models.dart @@ -0,0 +1,1060 @@ +class AuthenticatedUser { + const AuthenticatedUser({ + required this.userId, + required this.username, + required this.displayName, + required this.token, + required this.expiresAt, + }); + + factory AuthenticatedUser.fromJson(Map json) { + return AuthenticatedUser( + userId: json['userId']?.toString() ?? '', + username: json['username']?.toString() ?? '', + displayName: json['displayName']?.toString() ?? '', + token: json['token']?.toString() ?? '', + expiresAt: json['expiresAt']?.toString() ?? '', + ); + } + + final String userId; + final String username; + final String displayName; + final String token; + final String expiresAt; + + Map toJson() { + return { + 'userId': userId, + 'username': username, + 'displayName': displayName, + 'token': token, + 'expiresAt': expiresAt, + }; + } +} + +class LoginResponse { + const LoginResponse({ + required this.token, + required this.expiresAt, + required this.user, + }); + + factory LoginResponse.fromJson(Map json) { + return LoginResponse( + token: json['token']?.toString() ?? '', + expiresAt: json['expiresAt']?.toString() ?? '', + user: AuthenticatedUser.fromJson(_asMap(json['user'])), + ); + } + + final String token; + final String expiresAt; + final AuthenticatedUser user; + + Map toJson() { + return { + 'token': token, + 'expiresAt': expiresAt, + 'user': user.toJson(), + }; + } +} + +class LiveRoom { + const LiveRoom({ + required this.id, + required this.platform, + required this.platformName, + required this.sourceUrl, + required this.roomId, + required this.normalizedUrl, + required this.title, + required this.anchorName, + required this.anchorId, + required this.avatarUrl, + required this.coverUrl, + required this.remark, + required this.isPinned, + required this.alias, + required this.isPriority, + required this.pollingIntervalSecondsOverride, + required this.originalLiveRoomUrl, + required this.overrides, + required this.effectiveSettings, + required this.isEnabled, + required this.availabilityStatus, + required this.currentRecordingState, + required this.lastAutoStartDecisionCode, + required this.lastAutoStartDecisionSummary, + required this.lastAutoStartDecisionDetail, + required this.lastAutoStartDecisionAt, + required this.lastCheckedAt, + required this.createdAt, + required this.updatedAt, + }); + + factory LiveRoom.fromJson(Map json) { + return LiveRoom( + id: json['id']?.toString() ?? '', + platform: _asInt(json['platform']), + platformName: json['platformName']?.toString() ?? '', + sourceUrl: json['sourceUrl']?.toString() ?? '', + roomId: json['roomId']?.toString() ?? '', + normalizedUrl: json['normalizedUrl']?.toString() ?? '', + title: json['title']?.toString(), + anchorName: json['anchorName']?.toString(), + anchorId: json['anchorId']?.toString(), + avatarUrl: json['avatarUrl']?.toString(), + coverUrl: json['coverUrl']?.toString(), + remark: json['remark']?.toString(), + isPinned: json['isPinned'] as bool? ?? false, + alias: json['alias']?.toString(), + isPriority: json['isPriority'] as bool? ?? false, + pollingIntervalSecondsOverride: _asNullableInt(json['pollingIntervalSecondsOverride']), + originalLiveRoomUrl: json['originalLiveRoomUrl']?.toString() ?? '', + overrides: LiveRoomSettingsOverrides.fromJson(_asMap(json['overrides'])), + effectiveSettings: LiveRoomEffectiveSettings.fromJson(_asMap(json['effectiveSettings'])), + isEnabled: json['isEnabled'] as bool? ?? false, + availabilityStatus: _asInt(json['availabilityStatus']), + currentRecordingState: _asInt(json['currentRecordingState']), + lastAutoStartDecisionCode: json['lastAutoStartDecisionCode']?.toString(), + lastAutoStartDecisionSummary: json['lastAutoStartDecisionSummary']?.toString(), + lastAutoStartDecisionDetail: json['lastAutoStartDecisionDetail']?.toString(), + lastAutoStartDecisionAt: json['lastAutoStartDecisionAt']?.toString(), + lastCheckedAt: json['lastCheckedAt']?.toString(), + createdAt: json['createdAt']?.toString() ?? '', + updatedAt: json['updatedAt']?.toString() ?? '', + ); + } + + final String id; + final int platform; + final String platformName; + final String sourceUrl; + final String roomId; + final String normalizedUrl; + final String? title; + final String? anchorName; + final String? anchorId; + final String? avatarUrl; + final String? coverUrl; + final String? remark; + final bool isPinned; + final String? alias; + final bool isPriority; + final int? pollingIntervalSecondsOverride; + final String originalLiveRoomUrl; + final LiveRoomSettingsOverrides overrides; + final LiveRoomEffectiveSettings effectiveSettings; + final bool isEnabled; + final int availabilityStatus; + final int currentRecordingState; + final String? lastAutoStartDecisionCode; + final String? lastAutoStartDecisionSummary; + final String? lastAutoStartDecisionDetail; + final String? lastAutoStartDecisionAt; + final String? lastCheckedAt; + final String createdAt; + final String updatedAt; +} + +class LiveRoomSettingsOverrides { + const LiveRoomSettingsOverrides({ + required this.preferredQuality, + required this.outputFormat, + required this.saveMode, + required this.recordingTemplate, + required this.segmentDurationMinutes, + required this.enableAutoReconnect, + required this.reconnectDelayMaxSeconds, + required this.readWriteTimeoutMilliseconds, + required this.enableDanmakuRecording, + required this.danmakuIncludeNonChatEvents, + required this.danmakuMinPollIntervalMilliseconds, + required this.danmakuRetryDelayMaxSeconds, + }); + + factory LiveRoomSettingsOverrides.fromJson(Map json) { + return LiveRoomSettingsOverrides( + preferredQuality: json['preferredQuality']?.toString(), + outputFormat: _asNullableInt(json['outputFormat']), + saveMode: _asNullableInt(json['saveMode']), + recordingTemplate: _asNullableInt(json['recordingTemplate']), + segmentDurationMinutes: _asNullableInt(json['segmentDurationMinutes']), + enableAutoReconnect: json['enableAutoReconnect'] as bool?, + reconnectDelayMaxSeconds: _asNullableInt(json['reconnectDelayMaxSeconds']), + readWriteTimeoutMilliseconds: _asNullableInt(json['readWriteTimeoutMilliseconds']), + enableDanmakuRecording: json['enableDanmakuRecording'] as bool?, + danmakuIncludeNonChatEvents: json['danmakuIncludeNonChatEvents'] as bool?, + danmakuMinPollIntervalMilliseconds: _asNullableInt(json['danmakuMinPollIntervalMilliseconds']), + danmakuRetryDelayMaxSeconds: _asNullableInt(json['danmakuRetryDelayMaxSeconds']), + ); + } + + final String? preferredQuality; + final int? outputFormat; + final int? saveMode; + final int? recordingTemplate; + final int? segmentDurationMinutes; + final bool? enableAutoReconnect; + final int? reconnectDelayMaxSeconds; + final int? readWriteTimeoutMilliseconds; + final bool? enableDanmakuRecording; + final bool? danmakuIncludeNonChatEvents; + final int? danmakuMinPollIntervalMilliseconds; + final int? danmakuRetryDelayMaxSeconds; +} + +class LiveRoomEffectiveSettings { + const LiveRoomEffectiveSettings({ + required this.preferredQuality, + required this.outputFormat, + required this.saveMode, + required this.recordingTemplate, + required this.segmentDurationMinutes, + required this.enableAutoReconnect, + required this.reconnectDelayMaxSeconds, + required this.readWriteTimeoutMilliseconds, + required this.enableDanmakuRecording, + required this.danmakuIncludeNonChatEvents, + required this.danmakuMinPollIntervalMilliseconds, + required this.danmakuRetryDelayMaxSeconds, + }); + + factory LiveRoomEffectiveSettings.fromJson(Map json) { + return LiveRoomEffectiveSettings( + preferredQuality: json['preferredQuality']?.toString() ?? '', + outputFormat: _asInt(json['outputFormat']), + saveMode: _asInt(json['saveMode']), + recordingTemplate: _asInt(json['recordingTemplate']), + segmentDurationMinutes: _asInt(json['segmentDurationMinutes']), + enableAutoReconnect: json['enableAutoReconnect'] as bool? ?? false, + reconnectDelayMaxSeconds: _asInt(json['reconnectDelayMaxSeconds']), + readWriteTimeoutMilliseconds: _asInt(json['readWriteTimeoutMilliseconds']), + enableDanmakuRecording: json['enableDanmakuRecording'] as bool? ?? false, + danmakuIncludeNonChatEvents: json['danmakuIncludeNonChatEvents'] as bool? ?? false, + danmakuMinPollIntervalMilliseconds: _asInt(json['danmakuMinPollIntervalMilliseconds']), + danmakuRetryDelayMaxSeconds: _asInt(json['danmakuRetryDelayMaxSeconds']), + ); + } + + final String preferredQuality; + final int outputFormat; + final int saveMode; + final int recordingTemplate; + final int segmentDurationMinutes; + final bool enableAutoReconnect; + final int reconnectDelayMaxSeconds; + final int readWriteTimeoutMilliseconds; + final bool enableDanmakuRecording; + final bool danmakuIncludeNonChatEvents; + final int danmakuMinPollIntervalMilliseconds; + final int danmakuRetryDelayMaxSeconds; +} + +class RecordTask { + const RecordTask({ + required this.id, + required this.liveRoomId, + required this.recordSessionId, + required this.segmentIndex, + required this.liveRoomTitle, + required this.platform, + required this.roomId, + required this.status, + required this.preferredQuality, + required this.outputFormat, + required this.streamUrl, + required this.outputFilePath, + required this.recorderProcessId, + required this.errorMessage, + required this.createdAt, + required this.startedAt, + required this.endedAt, + required this.durationSeconds, + required this.postProcessStage, + required this.postProcessProgressPercent, + required this.postProcessDetail, + }); + + factory RecordTask.fromJson(Map json) { + return RecordTask( + id: json['id']?.toString() ?? '', + liveRoomId: json['liveRoomId']?.toString() ?? '', + recordSessionId: json['recordSessionId']?.toString() ?? '', + segmentIndex: _asInt(json['segmentIndex']), + liveRoomTitle: json['liveRoomTitle']?.toString() ?? '', + platform: _asInt(json['platform']), + roomId: json['roomId']?.toString() ?? '', + status: _asInt(json['status']), + preferredQuality: json['preferredQuality']?.toString() ?? '', + outputFormat: _asInt(json['outputFormat']), + streamUrl: json['streamUrl']?.toString(), + outputFilePath: json['outputFilePath']?.toString(), + recorderProcessId: _asNullableInt(json['recorderProcessId']), + errorMessage: json['errorMessage']?.toString(), + createdAt: json['createdAt']?.toString() ?? '', + startedAt: json['startedAt']?.toString(), + endedAt: json['endedAt']?.toString(), + durationSeconds: _asNullableNum(json['durationSeconds']), + postProcessStage: json['postProcessStage']?.toString(), + postProcessProgressPercent: _asNullableNum(json['postProcessProgressPercent']), + postProcessDetail: json['postProcessDetail']?.toString(), + ); + } + + final String id; + final String liveRoomId; + final String recordSessionId; + final int segmentIndex; + final String liveRoomTitle; + final int platform; + final String roomId; + final int status; + final String preferredQuality; + final int outputFormat; + final String? streamUrl; + final String? outputFilePath; + final int? recorderProcessId; + final String? errorMessage; + final String createdAt; + final String? startedAt; + final String? endedAt; + final num? durationSeconds; + final String? postProcessStage; + final num? postProcessProgressPercent; + final String? postProcessDetail; +} + +class RecordResult { + const RecordResult({ + required this.id, + required this.filePath, + required this.fileSizeBytes, + required this.durationSeconds, + required this.danmakuFilePath, + required this.danmakuMessageCount, + required this.finalStatus, + required this.errorMessage, + required this.uploadStatus, + required this.lastUploadProvider, + required this.remoteVideoPath, + required this.remoteDanmakuPath, + required this.lastUploadedAt, + required this.uploadErrorMessage, + required this.deletedLocalFilesAfterUpload, + required this.createdAt, + }); + + factory RecordResult.fromJson(Map json) { + return RecordResult( + id: json['id']?.toString() ?? '', + filePath: json['filePath']?.toString() ?? '', + fileSizeBytes: _asNullableNum(json['fileSizeBytes']), + durationSeconds: _asNullableNum(json['durationSeconds']), + danmakuFilePath: json['danmakuFilePath']?.toString(), + danmakuMessageCount: _asInt(json['danmakuMessageCount']), + finalStatus: _asInt(json['finalStatus']), + errorMessage: json['errorMessage']?.toString(), + uploadStatus: _asInt(json['uploadStatus']), + lastUploadProvider: json['lastUploadProvider']?.toString(), + remoteVideoPath: json['remoteVideoPath']?.toString(), + remoteDanmakuPath: json['remoteDanmakuPath']?.toString(), + lastUploadedAt: json['lastUploadedAt']?.toString(), + uploadErrorMessage: json['uploadErrorMessage']?.toString(), + deletedLocalFilesAfterUpload: json['deletedLocalFilesAfterUpload'] as bool? ?? false, + createdAt: json['createdAt']?.toString() ?? '', + ); + } + + final String id; + final String filePath; + final num? fileSizeBytes; + final num? durationSeconds; + final String? danmakuFilePath; + final int danmakuMessageCount; + final int finalStatus; + final String? errorMessage; + final int uploadStatus; + final String? lastUploadProvider; + final String? remoteVideoPath; + final String? remoteDanmakuPath; + final String? lastUploadedAt; + final String? uploadErrorMessage; + final bool deletedLocalFilesAfterUpload; + final String createdAt; +} + +class SystemLog { + const SystemLog({ + required this.id, + required this.level, + required this.category, + required this.message, + required this.detail, + required this.liveRoomId, + required this.recordSessionId, + required this.recordTaskId, + required this.createdAt, + }); + + factory SystemLog.fromJson(Map json) { + return SystemLog( + id: json['id']?.toString() ?? '', + level: _asInt(json['level']), + category: json['category']?.toString() ?? '', + message: json['message']?.toString() ?? '', + detail: json['detail']?.toString(), + liveRoomId: json['liveRoomId']?.toString(), + recordSessionId: json['recordSessionId']?.toString(), + recordTaskId: json['recordTaskId']?.toString(), + createdAt: json['createdAt']?.toString() ?? '', + ); + } + + final String id; + final int level; + final String category; + final String message; + final String? detail; + final String? liveRoomId; + final String? recordSessionId; + final String? recordTaskId; + final String createdAt; +} + +class RecordTaskDetail { + const RecordTaskDetail({ + required this.task, + required this.result, + required this.logs, + }); + + factory RecordTaskDetail.fromJson(Map json) { + return RecordTaskDetail( + task: RecordTask.fromJson(_asMap(json['task'])), + result: json['result'] == null ? null : RecordResult.fromJson(_asMap(json['result'])), + logs: _asList(json['logs']).map(SystemLog.fromJson).toList(growable: false), + ); + } + + final RecordTask task; + final RecordResult? result; + final List logs; +} + +class RecordSession { + const RecordSession({ + required this.id, + required this.liveRoomId, + required this.liveRoomTitle, + required this.platform, + required this.roomId, + required this.status, + required this.preferredQuality, + required this.outputFormat, + required this.saveMode, + required this.activeSegmentIndex, + required this.segmentCount, + required this.recorderProcessId, + required this.errorMessage, + required this.createdAt, + required this.startedAt, + required this.endedAt, + required this.totalFileSizeBytes, + required this.totalDanmakuMessageCount, + required this.tasks, + }); + + factory RecordSession.fromJson(Map json) { + return RecordSession( + id: json['id']?.toString() ?? '', + liveRoomId: json['liveRoomId']?.toString() ?? '', + liveRoomTitle: json['liveRoomTitle']?.toString() ?? '', + platform: _asInt(json['platform']), + roomId: json['roomId']?.toString() ?? '', + status: _asInt(json['status']), + preferredQuality: json['preferredQuality']?.toString() ?? '', + outputFormat: _asInt(json['outputFormat']), + saveMode: _asInt(json['saveMode']), + activeSegmentIndex: _asInt(json['activeSegmentIndex']), + segmentCount: _asInt(json['segmentCount']), + recorderProcessId: _asNullableInt(json['recorderProcessId']), + errorMessage: json['errorMessage']?.toString(), + createdAt: json['createdAt']?.toString() ?? '', + startedAt: json['startedAt']?.toString(), + endedAt: json['endedAt']?.toString(), + totalFileSizeBytes: _asNum(json['totalFileSizeBytes']), + totalDanmakuMessageCount: _asInt(json['totalDanmakuMessageCount']), + tasks: _asList(json['tasks']).map(RecordTask.fromJson).toList(growable: false), + ); + } + + final String id; + final String liveRoomId; + final String liveRoomTitle; + final int platform; + final String roomId; + final int status; + final String preferredQuality; + final int outputFormat; + final int saveMode; + final int activeSegmentIndex; + final int segmentCount; + final int? recorderProcessId; + final String? errorMessage; + final String createdAt; + final String? startedAt; + final String? endedAt; + final num totalFileSizeBytes; + final int totalDanmakuMessageCount; + final List tasks; +} + +class RecordSessionTimelineSegment { + const RecordSessionTimelineSegment({ + required this.recordTaskId, + required this.segmentIndex, + required this.status, + required this.startedAt, + required this.endedAt, + required this.offsetSeconds, + required this.durationSeconds, + required this.label, + required this.detail, + }); + + factory RecordSessionTimelineSegment.fromJson(Map json) { + return RecordSessionTimelineSegment( + recordTaskId: json['recordTaskId']?.toString() ?? '', + segmentIndex: _asInt(json['segmentIndex']), + status: _asInt(json['status']), + startedAt: json['startedAt']?.toString() ?? '', + endedAt: json['endedAt']?.toString() ?? '', + offsetSeconds: _asNum(json['offsetSeconds']), + durationSeconds: _asNum(json['durationSeconds']), + label: json['label']?.toString(), + detail: json['detail']?.toString(), + ); + } + + final String recordTaskId; + final int segmentIndex; + final int status; + final String startedAt; + final String endedAt; + final num offsetSeconds; + final num durationSeconds; + final String? label; + final String? detail; +} + +class RecordSessionTimelineEvent { + const RecordSessionTimelineEvent({ + required this.id, + required this.layer, + required this.title, + required this.detail, + required this.recordTaskId, + required this.segmentIndex, + required this.level, + required this.occurredAt, + required this.offsetSeconds, + }); + + factory RecordSessionTimelineEvent.fromJson(Map json) { + return RecordSessionTimelineEvent( + id: json['id']?.toString() ?? '', + layer: json['layer']?.toString() ?? '', + title: json['title']?.toString() ?? '', + detail: json['detail']?.toString(), + recordTaskId: json['recordTaskId']?.toString(), + segmentIndex: _asNullableInt(json['segmentIndex']), + level: _asNullableInt(json['level']), + occurredAt: json['occurredAt']?.toString() ?? '', + offsetSeconds: _asNum(json['offsetSeconds']), + ); + } + + final String id; + final String layer; + final String title; + final String? detail; + final String? recordTaskId; + final int? segmentIndex; + final int? level; + final String occurredAt; + final num offsetSeconds; +} + +class RecordSessionHeatBucket { + const RecordSessionHeatBucket({ + required this.recordTaskId, + required this.segmentIndex, + required this.bucketStartedAt, + required this.offsetSeconds, + required this.durationSeconds, + required this.messageCount, + }); + + factory RecordSessionHeatBucket.fromJson(Map json) { + return RecordSessionHeatBucket( + recordTaskId: json['recordTaskId']?.toString() ?? '', + segmentIndex: _asInt(json['segmentIndex']), + bucketStartedAt: json['bucketStartedAt']?.toString() ?? '', + offsetSeconds: _asNum(json['offsetSeconds']), + durationSeconds: _asNum(json['durationSeconds']), + messageCount: _asInt(json['messageCount']), + ); + } + + final String recordTaskId; + final int segmentIndex; + final String bucketStartedAt; + final num offsetSeconds; + final num durationSeconds; + final int messageCount; +} + +class RecordSessionTimeline { + const RecordSessionTimeline({ + required this.anchorAt, + required this.totalDurationSeconds, + required this.segments, + required this.events, + required this.heatBuckets, + }); + + factory RecordSessionTimeline.fromJson(Map json) { + return RecordSessionTimeline( + anchorAt: json['anchorAt']?.toString() ?? '', + totalDurationSeconds: _asNum(json['totalDurationSeconds']), + segments: _asList(json['segments']).map(RecordSessionTimelineSegment.fromJson).toList(growable: false), + events: _asList(json['events']).map(RecordSessionTimelineEvent.fromJson).toList(growable: false), + heatBuckets: _asList(json['heatBuckets']).map(RecordSessionHeatBucket.fromJson).toList(growable: false), + ); + } + + final String anchorAt; + final num totalDurationSeconds; + final List segments; + final List events; + final List heatBuckets; +} + +class RecordSessionDetail { + const RecordSessionDetail({ + required this.session, + required this.timeline, + required this.logs, + }); + + factory RecordSessionDetail.fromJson(Map json) { + return RecordSessionDetail( + session: RecordSession.fromJson(_asMap(json['session'])), + timeline: RecordSessionTimeline.fromJson(_asMap(json['timeline'])), + logs: _asList(json['logs']).map(SystemLog.fromJson).toList(growable: false), + ); + } + + final RecordSession session; + final RecordSessionTimeline timeline; + final List logs; +} + +class RecoveryOverview { + const RecoveryOverview({ + required this.storage, + required this.liveRooms, + required this.finalizations, + }); + + factory RecoveryOverview.fromJson(Map json) { + return RecoveryOverview( + storage: StorageGuardStatus.fromJson(_asMap(json['storage'])), + liveRooms: _asList(json['liveRooms']).map(RecoverableLiveRoom.fromJson).toList(growable: false), + finalizations: _asList(json['finalizations']).map(RecoverableFinalization.fromJson).toList(growable: false), + ); + } + + final StorageGuardStatus storage; + final List liveRooms; + final List finalizations; +} + +class StorageGuardStatus { + const StorageGuardStatus({ + required this.isEnabled, + required this.hasEnoughSpace, + required this.checkedPath, + required this.availableBytes, + required this.requiredBytes, + required this.message, + }); + + factory StorageGuardStatus.fromJson(Map json) { + return StorageGuardStatus( + isEnabled: json['isEnabled'] as bool? ?? false, + hasEnoughSpace: json['hasEnoughSpace'] as bool? ?? false, + checkedPath: json['checkedPath']?.toString() ?? '', + availableBytes: _asNum(json['availableBytes']), + requiredBytes: _asNum(json['requiredBytes']), + message: json['message']?.toString() ?? '', + ); + } + + final bool isEnabled; + final bool hasEnoughSpace; + final String checkedPath; + final num availableBytes; + final num requiredBytes; + final String message; +} + +class RecoverableLiveRoom { + const RecoverableLiveRoom({ + required this.liveRoomId, + required this.platformName, + required this.roomId, + required this.title, + required this.anchorName, + required this.lastAutoStartDecisionCode, + required this.lastAutoStartDecisionSummary, + required this.lastAutoStartDecisionDetail, + required this.lastAutoStartDecisionAt, + required this.lastCheckedAt, + }); + + factory RecoverableLiveRoom.fromJson(Map json) { + return RecoverableLiveRoom( + liveRoomId: json['liveRoomId']?.toString() ?? '', + platformName: json['platformName']?.toString() ?? '', + roomId: json['roomId']?.toString() ?? '', + title: json['title']?.toString(), + anchorName: json['anchorName']?.toString(), + lastAutoStartDecisionCode: json['lastAutoStartDecisionCode']?.toString(), + lastAutoStartDecisionSummary: json['lastAutoStartDecisionSummary']?.toString(), + lastAutoStartDecisionDetail: json['lastAutoStartDecisionDetail']?.toString(), + lastAutoStartDecisionAt: json['lastAutoStartDecisionAt']?.toString(), + lastCheckedAt: json['lastCheckedAt']?.toString(), + ); + } + + final String liveRoomId; + final String platformName; + final String roomId; + final String? title; + final String? anchorName; + final String? lastAutoStartDecisionCode; + final String? lastAutoStartDecisionSummary; + final String? lastAutoStartDecisionDetail; + final String? lastAutoStartDecisionAt; + final String? lastCheckedAt; +} + +class RecoverableFinalization { + const RecoverableFinalization({ + required this.recordTaskId, + required this.recordSessionId, + required this.liveRoomId, + required this.liveRoomTitle, + required this.roomId, + required this.platformName, + required this.segmentIndex, + required this.status, + required this.outputFilePath, + required this.reason, + required this.createdAt, + required this.endedAt, + }); + + factory RecoverableFinalization.fromJson(Map json) { + return RecoverableFinalization( + recordTaskId: json['recordTaskId']?.toString() ?? '', + recordSessionId: json['recordSessionId']?.toString() ?? '', + liveRoomId: json['liveRoomId']?.toString() ?? '', + liveRoomTitle: json['liveRoomTitle']?.toString() ?? '', + roomId: json['roomId']?.toString() ?? '', + platformName: json['platformName']?.toString() ?? '', + segmentIndex: _asInt(json['segmentIndex']), + status: _asInt(json['status']), + outputFilePath: json['outputFilePath']?.toString(), + reason: json['reason']?.toString(), + createdAt: json['createdAt']?.toString() ?? '', + endedAt: json['endedAt']?.toString(), + ); + } + + final String recordTaskId; + final String recordSessionId; + final String liveRoomId; + final String liveRoomTitle; + final String roomId; + final String platformName; + final int segmentIndex; + final int status; + final String? outputFilePath; + final String? reason; + final String createdAt; + final String? endedAt; +} + +class RecoveryActionResult { + const RecoveryActionResult({ + required this.requestedCount, + required this.successCount, + required this.failedCount, + required this.messages, + }); + + factory RecoveryActionResult.fromJson(Map json) { + return RecoveryActionResult( + requestedCount: _asInt(json['requestedCount']), + successCount: _asInt(json['successCount']), + failedCount: _asInt(json['failedCount']), + messages: _asStringList(json['messages']), + ); + } + + final int requestedCount; + final int successCount; + final int failedCount; + final List messages; +} + +class CleanupOperation { + const CleanupOperation({ + required this.id, + required this.kind, + required this.status, + required this.errorMessage, + required this.warnings, + }); + + factory CleanupOperation.fromJson(Map json) { + return CleanupOperation( + id: json['id']?.toString() ?? '', + kind: json['kind']?.toString() ?? '', + status: json['status']?.toString() ?? '', + errorMessage: json['errorMessage']?.toString(), + warnings: _asStringList(json['warnings']), + ); + } + + final String id; + final String kind; + final String status; + final String? errorMessage; + final List warnings; +} + +class MediaBrowserResponse { + const MediaBrowserResponse({ + required this.currentPath, + required this.parentPath, + required this.breadcrumbs, + required this.items, + }); + + factory MediaBrowserResponse.fromJson(Map json) { + return MediaBrowserResponse( + currentPath: json['currentPath']?.toString() ?? '', + parentPath: json['parentPath']?.toString(), + breadcrumbs: _asList(json['breadcrumbs']).map(MediaBrowserBreadcrumb.fromJson).toList(growable: false), + items: _asList(json['items']).map(MediaBrowserItem.fromJson).toList(growable: false), + ); + } + + final String currentPath; + final String? parentPath; + final List breadcrumbs; + final List items; +} + +class MediaBrowserBreadcrumb { + const MediaBrowserBreadcrumb({ + required this.label, + required this.relativePath, + }); + + factory MediaBrowserBreadcrumb.fromJson(Map json) { + return MediaBrowserBreadcrumb( + label: json['label']?.toString() ?? '', + relativePath: json['relativePath']?.toString() ?? '', + ); + } + + final String label; + final String relativePath; +} + +class MediaBrowserItem { + const MediaBrowserItem({ + required this.name, + required this.relativePath, + required this.type, + required this.sizeBytes, + required this.modifiedAt, + required this.canTranscode, + required this.canPreview, + }); + + factory MediaBrowserItem.fromJson(Map json) { + return MediaBrowserItem( + name: json['name']?.toString() ?? '', + relativePath: json['relativePath']?.toString() ?? '', + type: json['type']?.toString() ?? 'other', + sizeBytes: _asNullableNum(json['sizeBytes']), + modifiedAt: json['modifiedAt']?.toString(), + canTranscode: json['canTranscode'] as bool? ?? false, + canPreview: json['canPreview'] as bool? ?? false, + ); + } + + final String name; + final String relativePath; + final String type; + final num? sizeBytes; + final String? modifiedAt; + final bool canTranscode; + final bool canPreview; +} + +class RecordPreviewTicket { + const RecordPreviewTicket({ + required this.url, + required this.expiresAt, + }); + + factory RecordPreviewTicket.fromJson(Map json) { + return RecordPreviewTicket( + url: json['url']?.toString() ?? '', + expiresAt: json['expiresAt']?.toString() ?? '', + ); + } + + final String url; + final String expiresAt; +} + +class SystemSettings { + SystemSettings._(this._raw); + + factory SystemSettings.fromJson(Map json) { + return SystemSettings._(Map.from(json)); + } + + final Map _raw; + + Map toJson() => Map.from(_raw); + + SystemSettings copy() => SystemSettings._(toJson()); + + String get outputRoot => _raw['outputRoot']?.toString() ?? ''; + bool get enableStorageGuard => _raw['enableStorageGuard'] as bool? ?? false; + int get pauseRecordingWhenFreeSpaceBelowMegabytes => _asInt(_raw['pauseRecordingWhenFreeSpaceBelowMegabytes']); + int get resumeRecordingWhenFreeSpaceAboveMegabytes => _asInt(_raw['resumeRecordingWhenFreeSpaceAboveMegabytes']); + bool get enableRetentionCleanup => _raw['enableRetentionCleanup'] as bool? ?? false; + int get retentionDays => _asInt(_raw['retentionDays']); + bool get retentionDeleteFiles => _raw['retentionDeleteFiles'] as bool? ?? false; + String get retentionVideoFileCondition => _raw['retentionVideoFileCondition']?.toString() ?? 'any'; + List get retentionTaskStatuses => + _asDynamicList(_raw['retentionTaskStatuses']).map(_asInt).toList(growable: false); + bool get enableEmailNotification => _raw['enableEmailNotification'] as bool? ?? false; + String get emailToAddresses => _raw['emailToAddresses']?.toString() ?? ''; + bool get notifyOnLiveStarted => _raw['notifyOnLiveStarted'] as bool? ?? false; + bool get notifyOnException => _raw['notifyOnException'] as bool? ?? false; + bool get enableWebhookNotification => _raw['enableWebhookNotification'] as bool? ?? false; + String get webhookUrl => _raw['webhookUrl']?.toString() ?? ''; + int get webhookTimeoutSeconds => _asInt(_raw['webhookTimeoutSeconds']); + bool get notifyWebhookOnLiveStarted => _raw['notifyWebhookOnLiveStarted'] as bool? ?? false; + bool get notifyWebhookOnException => _raw['notifyWebhookOnException'] as bool? ?? false; + int get pollingIntervalSeconds => _asInt(_raw['pollingIntervalSeconds']); + bool get autoStartRecordingOnLive => _raw['autoStartRecordingOnLive'] as bool? ?? false; + + void updateNotificationSettings({ + required bool enableEmailNotification, + required String emailToAddresses, + required bool notifyOnLiveStarted, + required bool notifyOnException, + required bool enableWebhookNotification, + required String webhookUrl, + required int webhookTimeoutSeconds, + required bool notifyWebhookOnLiveStarted, + required bool notifyWebhookOnException, + }) { + _raw['enableEmailNotification'] = enableEmailNotification; + _raw['emailToAddresses'] = emailToAddresses; + _raw['notifyOnLiveStarted'] = notifyOnLiveStarted; + _raw['notifyOnException'] = notifyOnException; + _raw['enableWebhookNotification'] = enableWebhookNotification; + _raw['webhookUrl'] = webhookUrl; + _raw['webhookTimeoutSeconds'] = webhookTimeoutSeconds; + _raw['notifyWebhookOnLiveStarted'] = notifyWebhookOnLiveStarted; + _raw['notifyWebhookOnException'] = notifyWebhookOnException; + } +} + +Map _asMap(dynamic value) { + if (value is Map) { + return value; + } + if (value is Map) { + return value.map((Object? key, Object? item) => MapEntry(key.toString(), item)); + } + return {}; +} + +List> _asList(dynamic value) { + if (value is List) { + return value.map((dynamic item) => _asMap(item)).toList(growable: false); + } + return const >[]; +} + +List _asDynamicList(dynamic value) { + if (value is List) { + return List.from(value); + } + return const []; +} + +List _asStringList(dynamic value) { + if (value is List) { + return value.map((dynamic item) => item.toString()).toList(growable: false); + } + return const []; +} + +int _asInt(dynamic value) { + if (value is int) { + return value; + } + if (value is num) { + return value.toInt(); + } + return int.tryParse(value?.toString() ?? '') ?? 0; +} + +int? _asNullableInt(dynamic value) { + if (value == null) { + return null; + } + return _asInt(value); +} + +num _asNum(dynamic value) { + if (value is num) { + return value; + } + return num.tryParse(value?.toString() ?? '') ?? 0; +} + +num? _asNullableNum(dynamic value) { + if (value == null) { + return null; + } + return _asNum(value); +} diff --git a/mobile/lib/features/live_recorder/data/repositories/auth_repository.dart b/mobile/lib/features/live_recorder/data/repositories/auth_repository.dart new file mode 100644 index 0000000..3b10cd2 --- /dev/null +++ b/mobile/lib/features/live_recorder/data/repositories/auth_repository.dart @@ -0,0 +1,40 @@ +import 'package:live_recorder_mobile/core/network/api_client.dart'; +import 'package:live_recorder_mobile/features/live_recorder/data/models/live_recorder_models.dart'; + +class AuthRepository { + const AuthRepository(this._apiClient); + + final ApiClient _apiClient; + + Future login({ + required String username, + required String password, + }) async { + final response = await _apiClient.postJson( + '/api/auth/login', + body: { + 'username': username, + 'password': password, + }, + ); + return LoginResponse.fromJson(response as Map); + } + + Future logout() { + return _apiClient.postEmpty('/api/auth/logout'); + } + + Future changePassword({ + required String currentPassword, + required String newPassword, + }) { + return _apiClient.postEmpty( + '/api/auth/change-password', + body: { + 'currentPassword': currentPassword, + 'newPassword': newPassword, + }, + ); + } +} + diff --git a/mobile/lib/features/live_recorder/data/repositories/live_rooms_repository.dart b/mobile/lib/features/live_recorder/data/repositories/live_rooms_repository.dart new file mode 100644 index 0000000..b577e28 --- /dev/null +++ b/mobile/lib/features/live_recorder/data/repositories/live_rooms_repository.dart @@ -0,0 +1,72 @@ +import 'package:live_recorder_mobile/core/network/api_client.dart'; +import 'package:live_recorder_mobile/features/live_recorder/data/models/live_recorder_models.dart'; + +class LiveRoomsRepository { + const LiveRoomsRepository(this._apiClient); + + final ApiClient _apiClient; + + Future> listRooms() async { + final response = await _apiClient.getJson('/api/live-rooms') as List; + return response + .map((dynamic item) => LiveRoom.fromJson(item as Map)) + .toList(growable: false); + } + + Future getRoom(String roomId) async { + final response = await _apiClient.getJson('/api/live-rooms/$roomId') as Map; + return LiveRoom.fromJson(response); + } + + Future refreshRoom(String roomId) async { + final response = await _apiClient.postJson('/api/live-rooms/$roomId/refresh') as Map; + return LiveRoom.fromJson(response); + } + + Future setRoomEnabled({ + required String roomId, + required bool isEnabled, + }) async { + final response = await _apiClient.putJson( + '/api/live-rooms/$roomId/enabled', + body: {'isEnabled': isEnabled}, + ) as Map; + return LiveRoom.fromJson(response); + } + + Future createRoom({ + required String url, + int? platformOverride, + }) async { + final response = await _apiClient.postJson( + '/api/live-rooms', + body: { + 'url': url, + if (platformOverride case final int value) 'platformOverride': value, + }, + ) as Map; + return LiveRoom.fromJson(response); + } + + Future updateMetadata({ + required String roomId, + required Map payload, + }) async { + final response = await _apiClient.putJson( + '/api/live-rooms/$roomId/metadata', + body: payload, + ) as Map; + return LiveRoom.fromJson(response); + } + + Future updateSettings({ + required String roomId, + required Map payload, + }) async { + final response = await _apiClient.putJson( + '/api/live-rooms/$roomId/settings', + body: payload, + ) as Map; + return LiveRoom.fromJson(response); + } +} diff --git a/mobile/lib/features/live_recorder/data/repositories/logs_repository.dart b/mobile/lib/features/live_recorder/data/repositories/logs_repository.dart new file mode 100644 index 0000000..719c3ab --- /dev/null +++ b/mobile/lib/features/live_recorder/data/repositories/logs_repository.dart @@ -0,0 +1,34 @@ +import 'package:live_recorder_mobile/core/network/api_client.dart'; +import 'package:live_recorder_mobile/features/live_recorder/data/models/live_recorder_models.dart'; + +class LogsRepository { + const LogsRepository(this._apiClient); + + final ApiClient _apiClient; + + Future> listLogs({ + String? liveRoomId, + String? recordSessionId, + String? recordTaskId, + int? level, + String? content, + int take = 200, + }) async { + final response = await _apiClient.getJson( + '/api/logs', + queryParameters: { + if (liveRoomId != null && liveRoomId.isNotEmpty) 'liveRoomId': liveRoomId, + if (recordSessionId != null && recordSessionId.isNotEmpty) 'recordSessionId': recordSessionId, + if (recordTaskId != null && recordTaskId.isNotEmpty) 'recordTaskId': recordTaskId, + if (level != null) 'level': '$level', + if (content != null && content.trim().isNotEmpty) 'content': content.trim(), + 'take': '$take', + }, + ) as List; + + return response + .map((dynamic item) => SystemLog.fromJson(item as Map)) + .toList(growable: false); + } +} + diff --git a/mobile/lib/features/live_recorder/data/repositories/media_repository.dart b/mobile/lib/features/live_recorder/data/repositories/media_repository.dart new file mode 100644 index 0000000..98943be --- /dev/null +++ b/mobile/lib/features/live_recorder/data/repositories/media_repository.dart @@ -0,0 +1,39 @@ +import 'package:live_recorder_mobile/core/network/api_client.dart'; +import 'package:live_recorder_mobile/features/live_recorder/data/models/live_recorder_models.dart'; + +class MediaRepository { + const MediaRepository(this._apiClient); + + final ApiClient _apiClient; + + Future browse({String? path}) async { + final response = await _apiClient.getJson( + '/api/media/browser', + queryParameters: { + if (path != null && path.isNotEmpty) 'path': path, + }, + ) as Map; + return MediaBrowserResponse.fromJson(response); + } + + Uri buildFileUri({ + required String relativePath, + bool download = false, + }) { + return _apiClient.buildUri( + '/api/media/file', + queryParameters: { + 'path': relativePath, + if (download) 'download': 'true', + }, + ); + } + + Future transcodeFile(String relativePath) async { + final response = await _apiClient.postJson( + '/api/media/transcode-file', + body: {'relativePath': relativePath}, + ) as Map; + return response['message']?.toString() ?? '转码任务已提交'; + } +} diff --git a/mobile/lib/features/live_recorder/data/repositories/recordings_repository.dart b/mobile/lib/features/live_recorder/data/repositories/recordings_repository.dart new file mode 100644 index 0000000..0a868bb --- /dev/null +++ b/mobile/lib/features/live_recorder/data/repositories/recordings_repository.dart @@ -0,0 +1,82 @@ +import 'package:live_recorder_mobile/core/network/api_client.dart'; +import 'package:live_recorder_mobile/features/live_recorder/data/models/live_recorder_models.dart'; + +class RecordingsRepository { + const RecordingsRepository(this._apiClient); + + final ApiClient _apiClient; + + Future> listTasks({String? liveRoomId}) async { + final response = await _apiClient.getJson( + '/api/record-tasks', + queryParameters: { + if (liveRoomId != null && liveRoomId.isNotEmpty) 'liveRoomId': liveRoomId, + }, + ) as List; + return response + .map((dynamic item) => RecordTask.fromJson(item as Map)) + .toList(growable: false); + } + + Future getTaskDetail(String taskId) async { + final response = await _apiClient.getJson('/api/record-tasks/$taskId') as Map; + return RecordTaskDetail.fromJson(response); + } + + Future startRecording({ + required String liveRoomId, + required String preferredQuality, + required int outputFormat, + }) async { + final response = await _apiClient.postJson( + '/api/record-tasks/start', + body: { + 'liveRoomId': liveRoomId, + 'preferredQuality': preferredQuality, + 'outputFormat': outputFormat, + }, + ) as Map; + return RecordTask.fromJson(response); + } + + Future stopTask(String taskId) async { + final response = await _apiClient.postJson('/api/record-tasks/$taskId/stop') as Map; + return RecordTask.fromJson(response); + } + + Future createPreviewTicket(String taskId) async { + final response = await _apiClient.postJson('/api/record-tasks/$taskId/preview-ticket') as Map; + return RecordPreviewTicket.fromJson(response); + } + + Future uploadTask(String taskId) { + return _apiClient.postEmpty('/api/record-tasks/$taskId/upload'); + } + + Future> listSessions({String? liveRoomId}) async { + final response = await _apiClient.getJson( + '/api/record-sessions', + queryParameters: { + if (liveRoomId != null && liveRoomId.isNotEmpty) 'liveRoomId': liveRoomId, + }, + ) as List; + return response + .map((dynamic item) => RecordSession.fromJson(item as Map)) + .toList(growable: false); + } + + Future getSessionDetail(String sessionId) async { + final response = await _apiClient.getJson('/api/record-sessions/$sessionId') as Map; + return RecordSessionDetail.fromJson(response); + } + + Future stopSession(String sessionId) async { + final response = await _apiClient.postJson('/api/record-sessions/$sessionId/stop') as Map; + return RecordSession.fromJson(response); + } + + Future uploadSession(String sessionId) { + return _apiClient.postEmpty('/api/record-sessions/$sessionId/upload'); + } +} + diff --git a/mobile/lib/features/live_recorder/data/repositories/recovery_repository.dart b/mobile/lib/features/live_recorder/data/repositories/recovery_repository.dart new file mode 100644 index 0000000..b987951 --- /dev/null +++ b/mobile/lib/features/live_recorder/data/repositories/recovery_repository.dart @@ -0,0 +1,29 @@ +import 'package:live_recorder_mobile/core/network/api_client.dart'; +import 'package:live_recorder_mobile/features/live_recorder/data/models/live_recorder_models.dart'; + +class RecoveryRepository { + const RecoveryRepository(this._apiClient); + + final ApiClient _apiClient; + + Future getOverview() async { + final response = await _apiClient.getJson('/api/recovery') as Map; + return RecoveryOverview.fromJson(response); + } + + Future retryLiveRoom(String roomId) async { + final response = await _apiClient.postJson('/api/recovery/live-rooms/$roomId/retry') as Map; + return RecoveryActionResult.fromJson(response); + } + + Future retryAllLiveRooms() async { + final response = await _apiClient.postJson('/api/recovery/live-rooms/retry-all') as Map; + return RecoveryActionResult.fromJson(response); + } + + Future resumeFinalization(String taskId) async { + final response = await _apiClient.postJson('/api/recovery/finalizations/$taskId/resume') as Map; + return RecoveryActionResult.fromJson(response); + } +} + diff --git a/mobile/lib/features/live_recorder/data/repositories/settings_repository.dart b/mobile/lib/features/live_recorder/data/repositories/settings_repository.dart new file mode 100644 index 0000000..e5665cd --- /dev/null +++ b/mobile/lib/features/live_recorder/data/repositories/settings_repository.dart @@ -0,0 +1,27 @@ +import 'package:live_recorder_mobile/core/network/api_client.dart'; +import 'package:live_recorder_mobile/features/live_recorder/data/models/live_recorder_models.dart'; + +class SettingsRepository { + const SettingsRepository(this._apiClient); + + final ApiClient _apiClient; + + Future getSettings() async { + final response = await _apiClient.getJson('/api/settings') as Map; + return SystemSettings.fromJson(response); + } + + Future updateSettings(SystemSettings settings) async { + final response = await _apiClient.putJson( + '/api/settings', + body: settings.toJson(), + ) as Map; + return SystemSettings.fromJson(response); + } + + Future runRetentionCleanup() async { + final response = await _apiClient.postJson('/api/settings/retention/run-now') as Map; + return CleanupOperation.fromJson(response); + } +} + -- 2.39.2