import 'package:hgdj/config/address.dart' as address; import 'package:hgdj/hj_model/drama/drama_models.dart'; import 'package:hgdj/hj_model/video_model.dart'; import 'package:path/path.dart' as path; /// 作品的「一集」:漫画一话 / 小说一章 / 有声一集 / 短剧一集 共用这一个模型, /// 所以字段是几种业务的并集,取值时按自己的业务挑(如短剧只关心视频与解锁那几个) class MediaContent { // ===== 标识 ===== String? id; //本集 id String? mediaId; //所属作品(剧/漫画/小说)的 id int? episodeNumber; //第几集/第几话,从 1 开始 String? name; //集名,如「第1集」 String? cover; //本集封面,短剧多为空、回退用剧封面 // ===== 资源 ===== String? videoUrl; //视频相对路径(m3u8),要拼 baseApiPath 才能播 String? h265Url; //H.265 源,为空表示这一集没有 265 String? audioUrl; //有声书音频,见 realAudioUrl List? urlSet; //多清晰度地址集合,短剧下发空数组 String? text; //小说正文 String? md5; int? playTime; //时长(秒) int? mediaSize; //文件字节数 int? height; //视频高 int? weight; //视频宽(后端字段就叫 weight,不是重量) double? ratio; //宽高比,0 表示后端没算 // ===== 权限 / 付费 ===== //0会员 1金币购买 2免费。**只用于展示**,能不能播看 canPlay int? listenPermission; int? price; //单集金币价,0 为免费 bool? hasBuy; //本集是否已单独购买过 bool? isFree; //本集是否免费,别再用整部的 freeEpisode 自己算 bool? canPlay; //能不能播只认这个字段 String? accessType; // free 免费 / bought 已购 / card 短剧卡 / coin 未解锁 DramaPaywall? paywall; //付费墙文案与价格,canPlay=false 时必有,能播时为 null bool? isActive; //是否上架,下架的集不该出现在选集面板 // ===== 试看(feed / list / info 三个接口都下发)===== bool? previewEnabled; //这一集配没配试看 int? previewStart; //试看起播秒数,0 = 从头 int? previewSeconds; //试看时长(秒),从 previewStart 起算,到点停下挂付费墙 String? previewVideoUrl; //试看片地址,与 videoUrl 同格式 String? previewH265Url; //试看片的 265 源,为空表示没有 // ===== 统计 ===== int? countBrowse; int? countCollect; int? countComment; int? countDisLike; int? countLike; int? countPurchases; VidStatus? mediaStatus; //本集的已购/已收藏/已点赞状态 // ===== 时间 ===== String? createdAt; String? updateTime; //只用来造空 model 占位(拉详情失败时页面据此走错误态),字段一律走 fromJson 赋值 MediaContent(); MediaContent.fromJson(dynamic json) { json ??= {}; id = json['id']; mediaId = json['mediaId']; episodeNumber = json['episodeNumber']; name = json['name']; cover = json['cover']; videoUrl = json['videoUrl']; h265Url = json['h265Url']; audioUrl = json['audioUrl']; //用 is List 判而不是 as List:下发 null 或非数组时硬 cast 会抛,整条分集就解析不出来 final rawUrlSet = json['urlSet']; urlSet = rawUrlSet is List ? rawUrlSet.map((e) => e.toString()).toList() : []; text = json['text']; md5 = json['md5']; playTime = json['playTime']; mediaSize = json['mediaSize']; height = json['height']; weight = json['weight']; //数字直接转;后端也可能下发字符串("1.5"),那时才走 parse,脏值落 null final rawRatio = json['ratio']; ratio = rawRatio is num ? rawRatio.toDouble() : double.tryParse(rawRatio?.toString() ?? ''); listenPermission = json['listenPermission']; price = json['price']; hasBuy = json['hasBuy']; isFree = json['isFree']; canPlay = json['canPlay']; accessType = json['accessType']; paywall = json['paywall'] != null ? DramaPaywall.fromJson(json['paywall']) : null; isActive = json['isActive']; previewEnabled = json['previewEnabled']; previewStart = json['previewStart']; previewSeconds = json['previewSeconds']; previewVideoUrl = json['previewVideoUrl']; previewH265Url = json['previewH265Url']; countBrowse = json['countBrowse']; countCollect = json['countCollect']; countComment = json['countComment']; countDisLike = json['countDisLike']; countLike = json['countLike']; countPurchases = json['countPurchases']; mediaStatus = json['mediaStatus'] != null ? VidStatus.fromMap(json['mediaStatus']) : null; createdAt = json['createdAt']; updateTime = json['updateTime']; } /// 只给浏览历史落库用(经 CartoonMediaInfo.toJson → HistoryUtil)。 /// paywall 和 preview* 都是这次请求的临时状态、下次进来要重新拉,故意不存 Map toJson() { return { 'id': id, 'mediaId': mediaId, 'episodeNumber': episodeNumber, 'name': name, 'cover': cover, 'videoUrl': videoUrl, 'h265Url': h265Url, 'audioUrl': audioUrl, 'urlSet': urlSet, 'text': text, 'md5': md5, 'playTime': playTime, 'mediaSize': mediaSize, 'height': height, 'weight': weight, 'ratio': ratio, 'listenPermission': listenPermission, 'price': price, 'hasBuy': hasBuy, 'isFree': isFree, 'canPlay': canPlay, 'accessType': accessType, 'isActive': isActive, 'countBrowse': countBrowse, 'countCollect': countCollect, 'countComment': countComment, 'countDisLike': countDisLike, 'countLike': countLike, 'countPurchases': countPurchases, if (mediaStatus != null) 'mediaStatus': mediaStatus?.toJson(), 'createdAt': createdAt, 'updateTime': updateTime, }; } /// 有声书音频完整地址:相对路径要拼音频 CDN,已是 http(s) 的直接用。 /// 目前有声书播的是 ComicChapterInfo 里的同名实现,这里留着与 audioUrl 字段配套 String get realAudioUrl { final url = audioUrl; if (url?.isNotEmpty != true) return ''; if (url!.startsWith('http')) return url; return path.join(address.Address.audioCdnAddress ?? '', url); } }