ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

IPTVnator 下载管理器架构解析:从队列调度、断点续传到存档下载与录像恢复的全链路设计

IPTVnator 下载管理器架构解析:从队列调度、断点续传到存档下载与录像恢复的全链路设计 IPTVnator 下载管理器架构解析从队列调度、断点续传到存档下载与录像恢复的全链路设计【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnatorIPTVnator 的下载管理器Download Manager是一个桌面端专属子系统它把策展式队列 进度追踪 存储配置 播放控制层叠在现有 Xtream 与 Stalker 门户视图之上后端工作在 Electron 主进程中完成Angular 渲染层则暴露全局/workspace/downloads页面、按来源划分的子路由、上下文按钮与主题感知样式。本文基于仓库架构文档 download-manager.md 及对应源码完整拆解该系统的队列控制、Range 断点续传与重叠校验、存档catch-up下载的安全模型、渲染层服务模型与直播录像Live-TV recordings生命周期读完可以掌握一个生产级 Electron 下载子系统的完整设计思路。整体定位桌面功能双端分工下载管理器覆盖三类内容来源Xtream VOD/剧集、Stalker VOD/剧集以及 Xtream Live TV 的 catch-up 存档。分工边界非常清晰主进程Electron backend队列控制、字节传输、目标路径预留、文件固化finalize、目录授权、文件存在性探测、启动恢复渲染进程Angular web全局下载列表信号、路由作用域全局/按来源、队列与离线库分区视图、剧集季批量下载编排、离线详情页。两种门户libs/portal/xtream与libs/portal/stalker的视图只提供入口按钮与元数据准备下载本身完全走统一的桌面队列。Xtream 存档下载catch-up功能边界与提交路径桌面版 Xtream 直播节目对话框对已完成且可观看的 catch-up 节目提供Download programme (TS)操作时间线视图与列表视图都可用。交互上有几个关键约束宿主在解析 timeshift URL含面板时区之前会先捕获播放列表、频道与原始节目时间戳对话框打开期间切换频道会使操作失效点击下载不会选中该节目、也不会改变播放收藏视图保留复制存档 URL能力但存档下载目前仅通过 Xtream Live TV 暴露M3U、Stalker 与 PWA 的存档下载、HLS 拼接、定时录制未来的广播都不在此功能范围内。提交与校验由 EpgArchiveDownloadService 完成它把解析后的 URL 和该播放列表的请求头允许列表提交到现有桌面队列。要点包括待提交抑制是按节目身份粒度的慢解析不会丢掉另一个节目的请求HLS URL 被显式拒绝后端还会检查响应类型并逐包校验每个 188 字节 MPEG-TS 包不需要转码器或媒体辅助进程文件一律使用.ts扩展名只有已结束ended的节目才能入队已知的存档过期时间会在入队、重试、恢复、缺失文件恢复、传输启动五个时机分别检查。存档行的身份模型与数据库演进catchup类型额外存储programme_start节目开始 Unix 秒和一段 JSON 元数据频道名、开始/停止时间、已知的存档过期时间。其唯一身份是(playlist_id, xtream_id, programme_start)而电影与剧集保留原有身份。这一点可以在共享 schema 中得到确认download-tables.ts 中把原来单一的全局唯一索引拆成了两个条件唯一索引(table) ({ playlistIdx: index(downloads_playlist_idx).on(table.playlistId), statusIdx: index(downloads_status_idx).on(table.status), xtreamPlaylistUnique: uniqueIndex(downloads_xtream_playlist_unique) .on(table.xtreamId, table.playlistId, table.contentType) .where(sql${table.contentType} ! catchup), catchupUnique: uniqueIndex(downloads_catchup_unique) .on(table.xtreamId, table.playlistId, table.programmeStart) .where(sql${table.contentType} catchup), })数据库演进由 download-schema.ts 负责在旧的增量迁移之后它在事务中放宽 SQLite 的 CHECK 约束保留既有行 ID、文件、请求头、续传状态与元数据同时完成上述索引替换。存档传输的暂停与从字节零重启语义与普通下载不同catch-up 存档的resume/retry 永远从字节零重启——不使用 Range、If-Range也没有自动重连续接。暂停会保留其拥有的.part部分文件但队列界面会明确向用户解释这一点。这一设计在 download-catchup-transfer.ts 与 download-catchup-reservation.ts 中落实。文件写入的竞态防护download-catchup-output.ts 在截断前依次执行拒绝符号链接与硬链接、以不截断方式打开平台支持时加O_NOFOLLOW、把描述符身份与lstat对照然后才截断并通过同一描述符写入缺失的部分文件以排他方式创建使被替换的路径无法重定向写入。任务在整个提升promotion过程中持有该描述符的 device/inode 身份download-catchup-finalize.ts 会校验源文件与发布文件拒绝被替换的部分文件并在无硬链接的文件系统上从已验证的描述符执行拷贝。现有目标文件永远不会被覆盖。清理操作先把公共路径原子地移入私有临时目录再检查身份后删除捕获到的替换物以 no-clobber 方式恢复若恢复不可用或原路径被占用文件保留在.iptvnator-cleanup-*/entry中并记录恢复位置。清理绝不会递归删除非空隔离区。这套流程封闭了可预测路径上的 check/unlink 窗口但明确声明不隔离故意进入私有临时目录的同用户进程。写前日志WAL 式提升证明download_archive_finalizations存档安全模型的核心是一张写前 SQLite 日志表 download_archive_finalizations以下载 ID 为主键随下载级联删除export const downloadArchiveFinalizations sqliteTable( download_archive_finalizations, { downloadId: integer(download_id) .primaryKey() .references(() downloads.id, { onDelete: cascade }), proof: text(proof).notNull(), } );它在硬链接提升之前、或在排他创建的拷贝目标写入第一字节之前记录路径、大小、源身份与预期最终身份。于是即便是一个已完成但长度未知的文件在完成状态写入之前就有持久证明。由此衍生出完整的恢复矩阵启动恢复要求该证明且存在身份与大小匹配的常规文件才能把存档恢复为已完成在提升前终止则把已验证部分保持为暂停传输证明只有在校验 EOF 之后才升级为最终化证明进程内证明允许在完成状态的 DB 写入瞬时出错后立即恢复无需等待重启身份变化的日志部分文件被保留并与失败行脱钩Retry 会预留新路径而不是接管或截断它日志随已完成存档保留直到删除所有权信息以 BigInt 读取 device/inode日志存十进制字符串保证 64 位 Windows 文件引用不丢失并配合一个正的文件创建时间戳——unlink 后的 inode 复用无法给新条目盖章缺少创建时间的证明保持不可信新预留从排他创建描述符捕获身份并与下载行的路径/文件名在同一个 SQLite 事务中提交先于 HTTP 等待因此进程在传输建立前退出也可恢复。删除Remove/清除已完成Clear completed对存档部分文件使用日志支撑的私有捕获未知或被替换的条目一律保留无法清理时返回结构化恢复路径并打开持久化本地化对话框含完整路径、复制恢复路径按钮与手动恢复说明。存档传输的安全预算download-catchup-limits.ts 实现了三层预算源码可以逐条对照空闲超时 30 秒总截止时间为节目时长的两倍 10 分钟上限 24 小时字节预算取三者的最小值节目时长加一分钟后按 100 Mbit/s 的流量源码即(durationSeconds 60) * 12_500_000、64 GiB 硬上限、以及初始可用空间扣除 1 GiB 保留ARCHIVE_DISK_RESERVE后的一半——另一半是留给无硬链接文件系统上第二份拷贝的余量已知Content-Length超过预算的响应在写盘前直接拒绝未知长度响应先计数再转发分块每 16 MiBDISK_CHECK_INTERVAL复查一次剩余空间以应对磁盘上的其他活动输出流关闭后针对最终字节数再做一次拷贝余量检查覆盖低于该间隔的短尾保留的部分文件先通过已验证描述符安全截断再计算预算使 Resume/Retry 能复用其释放的空间这些限制仅适用于 TS 存档失败永远不会把部分文件提升到媒体库且错误信息中省略携带凭据的 URL。完成判定明确的不做清单完成意味着干净的 HTTP EOF、若提供Content-Length则与之一致、以及合法且非空的 TS 包封装。它不校验媒体时长也不校验提供商的节目边界——提供商可以返回一个更短的合法片段并以干净 EOF 结束当前版本无法在不解复用时长信息的情况下识别这一点。未知长度响应在 EOF 之前一直显示不定进度。已完成的存档卡片出现在 All 分区下携带节目标题、频道与播出日期点击卡片通过下载动作播放本地文件而不是导航到电影/剧集详情页。文件与元数据在重启之后、甚至来源存档过期之后依然可用。后端职责队列、传输与 IPC队列控制download-runtime.tsdownload-runtime.ts 是整个队列的中枢。DownloadTask镜像共享downloads表的一行类型Download定义于 download-tables.ts共享任务类型在download-task.ts另含瞬态的取消/暂停/进度辅助字段。各模块的分工文件职责download-requests.ts请求校验与行创建download-resume-requests.ts重试/恢复流程download-removal-requests.ts删除/终结清理downloads.events.ts仅负责 IPC 注册download-transfer.ts字节传输本体download-finalize.ts固化与保留部分文件持久化download-file-finalize.ts普通文件提升download-catchup-completion.ts存档完成download-catchup-recover-completion.ts有证明的完成恢复download-runtime-reservation.ts目标路径预留download-runtime-persistence.ts取消/暂停持久化download-broadcast.ts渲染层更新广播enqueueDownload()把任务推入内存中的downloadQueue并触发processQueue()processQueue()始终保持至多一个活动下载把行更新为downloading后调用startDownload()。Range 感知的传输download-transfer.ts传输通过后端已校验的 Axios 重定向辅助流式处理响应而非electron-dl并始终请求Accept-Encoding: identity——原因是 Range 偏移、总量与持久化的.part必须描述同一表示而 Axios 透明的 gzip/brotli 解码会把解码后字节写进磁盘却让所有计数器按编码字节计。请求头user agent、referer、origin持久化在request_headers中重试/恢复时经同一允许列表重新应用。新的 Xtream 电影与剧集下载会传播播放列表配置的头优先使用其 User-Agent否则与 Xtream API 请求和流探测共用XTREAM_CLIENT_USER_AGENT历史无 User-Agent 的 Xtream 行在重试/恢复/缺失恢复时会按所属播放列表类型补上该回退值已知 Stalker 行保持不变。由于下载行有意独立于被删除的播放列表存活那些来源已不存在、无法恢复其类型的旧无头行也会拿到同一 IPTV 播放器回退值。暂停/取消通过AbortController中止当前请求暂停保留部分文件取消删除它。恢复时先检查现有.part大小拒绝非常规文件所以暂停期间被植入的符号链接绝不会被跟随。有验证器validator的恢复首个响应的强ETag或Last-Modified持久化在resume_validator携带该验证器的部分文件以Range: bytesoffset-If-Range恢复由服务器本身证明实体未变。无验证器的恢复——重叠校验download-overlap.ts大量 Xtream 面板两个头都不发于是Range请求向前回退至多 256 KiB常量OVERLAP_VERIFICATION_BYTES 262_144变换流把重放窗口与部分文件尾部逐字节比对通过后才允许续写——相当于为这类面板自制了一个验证器。重叠不匹配会截断.part并从字节零重启OverlapMismatchError小于重叠窗口的部分文件通过普通请求从字节零完整验证后追加绝不原地重写重连夭折只会让文件变长。成功要求验证器消费完整窗口响应在窗口内结束属于普通保留型中断但在窗口内交付了完整权威总长的响应则证明远端实体缩短了必须从头重来——旧后缀永远不会被固化为已完成文件。HTTP 416 的分类classifyRangeNotSatisfiable()只有精确 EOF 请求 身份证明 声明的bytes */N等于部分文件大小才判定为完成只有声明的总量证明实体缩短才判定为重启其余一切无长度、模糊或矛盾的 416 一律保留部分文件且这些路径永远不会触达通用清理。验证器提升与总量承诺由完整重叠匹配提升的验证器在中途追加失败时同样生效错误路径也执行提升保留失败与暂停持久化都会从任务写回resume_validator使后续尝试走If-Range而不是重放窗口。验证器与响应总量在重叠匹配之前都保持未承诺状态过早给未验证字节盖上验证器会让下次恢复If-Range续接到一个外部前缀上过早持久化与未验证部分文件相等的总量会让已完成部分文件快捷路径在暂停/崩溃/保留失败后固化未验证字节。重叠重放从回退偏移重新计数字节因此续写时进度显示不低于保留大小。206必须以请求偏移开始校验Content-Range才能追加其他 2xx 响应服务器忽略Range或If-Range检测到远端文件已变让传输在同一.part上从字节零重启而不是让下载失败。目标路径冲突策略现有目标文件从不被覆盖、检查或删除新传输开始前后端原子预留一个可用的带编号.part路径同时保持最终目标路径不存在选定的最终filePath/fileName在传输开始前持久化若保留下载记录的目标在暂停/失败期间被占用例如用户自己创建了同名文件保留的.part被改名挪开并固化到下一个可用编号目标如Movie (1).mp4绝不按大小判断或unlink()解决冲突完成时从.part创建最终文件且不覆盖已有文件取消与不可恢复的传输失败删除.part而固化失败、完成部分文件失败、以及已落盘字节后的允许列表网络中断则故意保留行保留filePath后续重试无需重新下载即可收尾暂停与重启恢复也保留部分文件详情页对这类失败行发起的重新下载DOWNLOADS_START会先删除保留的.part再重置行。派生文件就绪检查与恢复DOWNLOADS_GET_LIST与DOWNLOADS_GET在每次读取时都会检查已完成目标只有常规非符号链接文件才报告为可用缺失路径、目录、符号链接与检查错误都报告为缺失但不修改持久化的completed传输状态。DOWNLOADS_REDOWNLOAD_MISSING只接受托管行 ID重新检查文件后若文件已重新出现则直接返回恢复结果无网络访问否则条件性地占用该完成行、保留其拥有目标、重新应用存储的头允许列表与 URL 安全策略并排队一个不会覆盖现有文件的新传输。IPC 面后端暴露DOWNLOADS_*处理器覆盖列表读取、开始/暂停/恢复/取消/重试/缺失文件恢复/删除操作、文件夹选择与在文件管理器中显示以及渲染层监听以刷新信号存储的DOWNLOADS_UPDATE_EVENT发射器。渲染层架构下载服务全局权威列表DownloadsService 中的downloads是渲染层权威的全局列表。loadDownloads()总是调用不带旧版可选播放列表作用域的 Electron 列表 IPC路由作用域、分类与搜索永远不能替换或收窄该信号。加载是串行化的至多一个列表 IPC 活动期间到达的调用方合并到一个尾随刷新之后因此每个响应按请求顺序提交被分配到尾随刷新的调用方在该刷新结束时才解析——即使后续广播又排入了下一轮刷新——同时防止陈旧延续与频繁进度更新下的饥饿。hasLoadedDownloads记录最近一次尝试含错误已完成hasAuthoritativeDownloadList在成功请求后为真、后续刷新飞行期间保持为真、且仅当最近请求失败时清除剧集下载动作要求两者同时为真使失败的刷新无法基于陈旧/空快照重启行每次新下载或恢复前服务先向主进程请求已授权文件夹再调用对应 IPC 命令onDownloadsUpdate广播触发一次新的全局加载。剧集季队列SeasonDownloadCoordinatorSeasonDownloadCoordinator拥有同步的按身份预留的待处理集合通过DownloadsService.startDownload()提交单个剧集或所选季的快照。要点季的批量提交是串行且尽力而为的一个候选失败不阻止后续候选添加或稳定重复提交之后一次权威列表刷新关闭待处理→排队/已下载的交接协调器返回added、skipped、failed计数Xtream 与 Stalker 适配器各自拥有提供商 URL、请求头与元数据准备协调器只做提供商无关的编排当预留候选匹配已完成但文件缺失的行时协调器在任何提供商准备之前做一次权威预检刷新若其他列表请求正在进行预检加入唯一的尾随刷新后续下载更新广播不会推迟已分配的刷新——恢复的文件因此成为稳定跳过无需发起 Stalker URL/网络请求两个提供商都把规范化episode.id作为规范剧集xtreamIdStalker 的originalCmd/originalId只参与 URL 解析。适配器保留数值季 0包括回退季键0使 Specials 保持独立的S00坐标精确的(playlistId, contentType, xtreamId)身份是权威的完整的(playlistId, seriesXtreamId, seasonNumber, episodeNumber)坐标只是遗留兼容回退。Stalker 还存episode_identity_scope普通/series、内嵌 VODseries[]、惰性 Ministra VODis_series三类来源已知不同 scope 视为不同剧集归属者无法证明 scope 的旧坐标行失败关闭而不是跨模式迁移渲染层待处理、排队、下载中、暂停的剧集被跳过文件可用或可用性未知的完成行也被跳过失败、取消、完成缺失与无歧义无行的剧集有资格完成缺失行以全新下载重启重置这类完成行之前DOWNLOADS_START在主进程异步重查其保留路径文件已恢复则返回稳定的reason: already-downloaded无变更活动匹配返回reason: already-in-progress。该重查有 1 秒调用方截止期在获取共享槽之前开始超时或探测失败保持行不变并返回失败提交让串行季循环继续文件探测只有ENOENT与ENOTDIR是权威缺失权限、I/O 与其他探测错误保持未知DOWNLOADS_START不动完成行与文件路径。清理.part时同路径工作合并底层 unlink 至多四个1 秒准入截止期拒绝排队中的工作一旦 unlink 开始就等待其权威结果避免迟到的副作用与重试竞态没有批量 IPC、没有并行传输、没有队列重排目录授权、持久化头处理与后端单活动传输 FIFO语义保持不变。纯管理器模型与下载工作区纯视图模型 download-manager.viewmodel.ts 与download-library.viewmodel.ts从全局信号派生当前路由作用域、搜索/分类过滤、队列分区、稳定排序、计数与受跟踪字节总量且不修改服务状态。queued/downloading/paused行构成活动面failed/canceled构成关注面文件缺失的completed行以专门恢复原因进入关注面只有可用的完成行进入离线库。带有效seriesXtreamId的已完成剧集按(playlistId, seriesXtreamId)分组、按季和剧集排序由一张海报卡代表无可用剧集 ID 的剧集保持独立卡片永不消失。下载工作区 把编排放在DownloadsComponent、异步变更放在组件作用域的DownloadManagerActionsService、来源路由解析放在DownloadLibraryNavigationService渲染则交给表现型组件DownloadQueueComponent、DownloadLibraryComponent与DownloadedSeriesDialogComponent——表现型子组件只发射类型化的DownloadItemAction不注入下载服务、路由、对话框或 snackbar。布局上固定头部与过滤行位于唯一的垂直滚动容器之上队列区与库区共享同一标题样式仪表盘侧栏标题风格加.app-count-badge计数队列行只暴露一个可见主操作暂停、恢复或重试取消、复制 URL、删除放在溢出菜单中避免两个相邻破坏性图标竞争。已完成电影与分组剧集复用门户规范内容网格的紧凑海报卡类型徽标 常显 ⋮ 触发器压在画面上标题与剧集事实在其下Play/在文件夹中显示/复制 URL/删除在溢出菜单中文件尺寸不重复展示属于离线详情页。卡片与加载骨架都消费全局--cover-grid-min-width/--cover-gaptoken使 Small/Medium/Large 封面偏好与全局一致所有表面使用现有--app-*与 Material 系统 token。诚实的交互语义每个异步条目命令持有 pending ID 直到 IPC 结果落地防止重复派发且不乐观地改状态服务失败走既有 snackbar 路径。删除已完成条目时明确告知已固化媒体文件保留在磁盘上删除暂停/失败/取消条目时说明保留的部分数据将被删除Clear finished 同时传达两种结果并在来源路由下保留播放列表作用域。VOD 与剧集详情页把暂停下载继续渲染为活跃的 Resume 按钮DownloadsService.isPaused()/resumeDownloadByContent()。所有完成卡片电影、分组剧集、独立剧集的封面与标题都打开该下载的聚焦离线详情溢出菜单中的显式 Play 才直接播放本地文件。缺失的完成行在 Needs attention 下显示File missing与Download again并 withholding Play 与在文件夹中显示。返回File not found的文件操作竞态会刷新权威列表并把用户从聚焦详情带回到管理器。聚焦离线详情Focused offline details)就绪的电影与分组剧集打开本地聚焦详情视图而非提供商目录页电影暴露本地 Play 与在文件夹中显示剧集只投影其已固化文件仍然可用的完成剧集行按季分组每个 Play/在文件夹中显示都指向该行本地文件。提供商的其他季与剧集刻意缺席于该视图View in portal对 Xtream 解析精确的分类/条目路由对 Stalker 只接受原始电影/普通剧集/VOD 剧集标记与下载类型一致的最近浏览快照使重叠的电影与剧集 ID 无法选中错误条目。下载快照中存储的精确数值分类优先于最近集合的虚拟vod/series分类无匹配时只有携带该精确数值分类的电影能形成仅元数据目标剧集与无该分类的遗留电影则不提供跳转。常规提供商详情以一次性provider-only呈现打开宿主可解析时保留提供商内容与播放隐藏本地、Offline 与下载动作不再本地可用的完成行永远不会渲染为就绪离线详情直接访问或过期详情 URL 返回管理器导航失败时详情壳显示带 Back 与 Retry 的缺失文件错误无效/已删除的下载 ID 渲染聚焦的未找到状态电影与剧集下载在开始时捕获版本化的、仅显示用的元数据快照来自已渲染的 Xtream 或 Stalker 详情含既有 TMDB 字段快照把提供商身份/分类与展示元数据分开使来源门户不可用时离线视图仍可渲染分组剧集选择最新的有效父快照只用较旧有效成员快照补齐缺失的父元数据较新值、按剧集元数据、语言与新鲜度身份保持权威遗留行与稀疏/陈旧/错误语言快照在聚焦详情打开时回填行元数据提供安全本地回退可解析时合并提供商数据可选 TMDB 增强使用与提供商详情相同的应用语言与合并规则成功刷新持久化到代表行瞬时失败保留安全改进而不把快照误标为新鲜并发刷新按请求排序、只有最新代可写入快照封面只接受安全的 HTTP(S) 图像 URL渲染层规范化与主进程持久化各自独立拒绝凭据形态的路径与查询键包括username使提供商凭据无法被缓存在离线元数据里。全局 API 面与路由main.preload.ts 把每个下载 IPC 命令加onDownloadsUpdate监听器都挂到window.electron上共享的ElectronBridgeApi契约electron-api.interface.ts拥有下载与播放进度方法类型根目录 global.d.ts 与 apps/web/src/typings.d.ts 引用该契约而不是重新声明桥接。路由结构全局工作区/workspace/downloads来源作用域变体/workspace/xtreams/:id/downloads与/workspace/stalker/:id/downloads。三者渲染同一个全局 store:id路由只是派生一个仅查看的播放列表作用域每个作用域暴露同样的聚焦子路由/workspace/downloads/:downloadId、/workspace/xtreams/:id/downloads/:downloadId、/workspace/stalker/:id/downloads/:downloadId。工作区壳把三者都视为聚焦内容路由搜索禁用、上下文面板为none所以本地专属条目旁边不会出现提供商分类管理器用replaceUrl把 All/Movies/Series/In progress 过滤持久化在当前路由查询中打开聚焦条目时保留该精确作用域 URL 作为已验证返回目标Back 即可恢复作用域、搜索、过滤与浏览器历史位置导航是数据驱动的portal-rail-links.ts 为两种门户都发射downloads分区链接path: [...root, downloads]复用同一个下载页。队列、持久化与 UX 细节每个下载行写入共享downloads表状态枚举为queued/downloading/paused/completed/failed/canceled元数据包括bytesDownloaded、totalBytes、errorMessage、requestHeaders、resumeValidator、离线详情元数据快照与 Xtream 标识。下载是本地拥有的记录而非播放列表子项删除来源保留其行与本地文件、保留在全局库中可见并禁用提供商跳转直到来源再次存在。启动时会重建仍携带播放列表外键的旧表增量列走幂等列迁移。列表与聚焦详情读取用主进程异步lstat探测验证完成文件一个共享探测队列至多 4 个文件系统调用并发只合并同路径的在飞检查不缓存已完成结果——外部删除在下一次刷新可见同时不让频繁进度广播阻塞主线程。启动恢复download-recovery.ts带非空.part的陈旧downloading行转为paused陈旧queued行转paused并保留.part排在活动下载后面等待恢复的行会以queued加部分文件的形式持久化无可恢复部分字节的陈旧downloading行标记为failed。队列取消删除排队任务或记录活动取消请求并中止请求暂停走同一中止路径但持久化paused并保留.part。重试复用同一数据库条目带保留filePath的失败行通过 HTTP Range 续传其.part否则从零开始。恢复在存了验证器时用If-Range否则用 256 KiB 重叠校验。无法删除的.part被锁定、权限拒绝永远不丢数据库路径取消以保留filePath的方式持久化canceled供日后清理DOWNLOADS_REMOVE保留行并回答success: false以 snackbar 呈现锁释放后重试删除即可。恢复以条件更新原子地占用行paused→queued且运行时队列拒绝重复 ID——两次快速恢复点击与状态刷新竞态也绝不可能产生同一下载的两个传输。在宣告表示大小之前干净结束的响应例如按响应封顶的代理永远不会被提交为完成传输以Transfer ended before the advertised size失败保留.part与filePath重试经 Range 从中断处继续。允许列表内的网络失败保留任何非空.part重叠校验拥有恢复正确性下次尝试可以对保留字节安全地证明、续传或重启——删除字节是唯一不可恢复的结果因此保留不需要总量、验证器或范围证据。被磁盘字节证伪的总量持久化为未知而非证伪值。失败行暴露稳定的DOWNLOAD_NETWORK_INTERRUPTED (code)消息不含 URLRetry 走同一恢复验证。只有非网络错误、全新空失败、以及中途缩小的部分文件保留通用失败路径。运行时会自动重连中断的传输download-reconnect.ts包裹在startDownload的传输外可恢复中断或干净的短响应触发 1 秒延迟的自动重连——因为按 N 字节/秒封顶每个连接的服务器Xtream 面板常见的反下载节流否则需要用户每约 130 MB 手动点一次 Retry。循环在结构上有界一次尝试必须比上次至少前进 64 KiB 才能重置停顿预算连续三次没有该进度就把最后中断作为普通保留失败暴露。重启是显式信号传输层每次从字节零重写.part重叠不匹配、实体缩短、HTTP 416、服务器忽略Range都会递增task.transferRestarts循环据此开启新进度纪元干净的停顿预算、无基线每次传输至多容忍两次重启未信号的字节回退算普通停顿。每次重连前后重新检查取消/暂停在收到任何响应前就失败的重连如对重启中的面板的ECONNREFUSED被转换为同一保留型中断自动重连因此永远不会销毁用户没碰过的多 GB 部分文件。总量处理区分权威与信息响应自身总量是唯一授权完成的东西不定范围宣告的端点bytes X-Y/*→ Y1只标记短交付无范围无总量的响应才保持未知长度 HTTP 的干净 EOF 完成契约携带自早前响应的总量只是信息性的——磁盘字节证伪它、或不定范围甚至可能到达它严格小于守卫时就被丢弃绝不安置最终化。数据库中记录的保留filePath在用户切换下载文件夹后仍然可用——恢复/重试保留行不再要求文件夹是当前选择新下载仍对当前所选文件夹做授权。启动恢复识别创建最终文件与提交行之间崩溃的固化downloading行、无部分文件、最终文件以记录大小存在标记为completed而不是失败并孤立文件。暂停/恢复端到端覆盖于 downloads.e2e.ts限流的 Range 模拟服务器验证磁盘上的暂停.part、Range/If-Range恢复请求与最终文件的逐字节组装。自动重连覆盖于 download-reliability.e2e.ts带验证器的中断服务器必须无需手动 Retry 通过Range/If-Range完成无验证器的中断服务器必须通过回退重叠校验的Range请求完成。系统下载路径始终已授权自定义文件夹只有在原生文件夹选择之后才成为已授权主进程把选择持久化到 ElectronuserData下。渲染层设置可以显示路径但不作为授权依据。管理器的搜索与 All/Movies/Series/In progress 过滤只影响可见实体受跟踪字节汇总刻意忽略搜索/分类过滤但尊重路由作用域——隐藏一张卡片不会让它看起来磁盘占用消失了。直播录像Live-TV recordings与下载并存但不属于下载用内嵌 MPV 播放器做的录像住在下载旁边而不是下载里面录像没有可重新获取的源 URL、没有字节总量、没有重试/恢复语义因此拥有自己的recordingsSQLite 表定义见 schema.ts没有唯一索引——重录一个频道是常态playlist_id无外键录像在来源播放列表删除后存活playlist_name作为playlistDisplayLabel显示快照保存状态枚举recording/completed/interrupted/failed行携带owner_pid供启动修复判断归属。生命周期追踪EmbeddedMpvRecordingTrackerembedded-mpv-recording-tracker.ts 拥有生命周期EmbeddedMpvNativeService中的显式 start/stop 钩子加会话快照观察者。关键不变量stop 钩子是请求而非结果——addon.stopRecording()只是分派异步 mpv 属性设置或写入 frame-copy helper 的命令所以最终化必须等待报告录像非活动的快照过早 stat/unlink 会把短录像报为失败并可能删掉 mpv 仍在冲刷的字节macOS 原生视图在分派异步属性设置之前清除recordingActive请求被拒绝时再恢复因此非活动快照还必须存活 1.5 秒安定窗口三个轮询周期才算确认被唤醒的录像取消待定的最终化10 秒硬界确保确认永远不来时也能最终化。同一观察者覆盖从不调用stopRecording()的停止路径流替换自动停止、frame-copy helper 崩溃、会话错误/关闭合成错误/关闭快照到达时拆除可能仍在冲刷helper 在disposeSession()后最多再活约 2 秒因此最终化被推迟到 2.5 秒冲刷窗口之后行在窗口内保持可启动修复的recording状态迁移recording→completed确认停止 fs.stat大小/interrupted隐式停止但有可播放部分文件——MPEG-TS 是可流的/failed启动错误或缺失/空文件。只有从未进入活动状态的录像才 unlink 其空预保留文件一旦 mpv 拥有了文件字节就不再被触碰。启动修复reconcileStaleRecordingsrecording-recovery.ts 的reconcileStaleRecordings在硬杀把行留在recording时解析它同时跳过另一活动实例仍拥有的行IPTVNATOR_ALLOW_MULTIPLE_INSTANCES以及追踪器自己仍在追踪的行activeRowIds()——渲染层在该路径运行前就已可交互所以引导期间开始的录像携带本进程自己的 pid只有追踪器台账能证明它存活。裸的 pid 存活不是所有权持有者还必须看起来像 IPTVnator/Electron 进程ps/tasklist名尽力而为且必须不能证明其启动晚于录像ps -o etime/ PowerShellStartTime——pid 只在旧拥有者死亡后释放所以被回收 pid 的持有者永远比录像年轻即使它落在另一个 Electron 应用上不可读的证据保持保守并跳过。修复改动一次广播RECORDINGS_UPDATE_EVENT因为渲染层可能已持有修复前的列表。修复与追踪器最终化都通过有界异步探测 stat3 秒截止期只有ENOENT/ENOTDIR证明缺失修复把无法判断的行留在recording留给下次启动最终化保留请求的状态但大小未知——位于死亡网络挂载上的录像目录既不能阻塞主线程/队列也不能把一个很可能完好的文件标成failed。修复批量并发探测main.ts大约等待一个截止期而非每行一个。列表装饰同样保留不确定的探测ElectronRecordingItem.fileAvailability增加unknown消费方以 missing门槛判断只有被证明的缺失才进入 Needs attention 或隐藏文件动作。追踪器最终化绑定到精确的打开条目而非可复用的会话 ID同一会话上停止→立即重启让旧条目归自己的安定定时器管替换 map 条目时若从未观察到停止则给新条目武装定时器因此两个行都不会被对方的生命周期最终化。元数据在录像开始时捕获EPG 是时间敏感的、Xtream/Stalker EPG 从不进 SQLite所以每个直播宿主M3U 播放器、Xtream live 布局、Stalker ITV 布局、统一直播标签页在开始时组装RecordingStartMetadata频道名/标志、播放列表 ID 显示标签快照、来源类型、EPG 键、当前节目流经WebPlayerViewComponent → EmbeddedMpvPlayerComponent → EmbeddedMpvControlsAdapter → EmbeddedMpvRecordingStartOptions.metadata。EmbeddedMpvPlayerComponent监视会话快照的 active→inactive 录像边并发射recordingStopped——每个触发源的唯一属主包括在下载管理器里点击的 Stop它直接与主进程通信永远到不了播放器自己的开关。事件携带录像活动期间捕获的 EPG 键因为切台会自动停止录像宿主状态在处理停止时已描述新频道每个宿主只在该键仍匹配当前频道时做增强。M3U 项的 EPG 键不唯一tvgId或显示名回退可被多条目共享所以 M3U 选择额外设置RecordingStartMetadata.sourceItemKey统一标签的item.uid、M3U 播放器的channel.id同样捕获并随停止事件携带、增强前比对防止在两个同键条目间切换时把第二个条目的日程贴到第一个条目的录像上。宿主以停止增强作答把内存节目列表过滤到与[startedAt, endedAt]重叠的节目iptvnator/shared/interfaces中的filterRecordingProgramsOverlap通过RECORDINGS_UPDATE_PROGRAMS发送按唯一目标路径键控——这就是跨越节目边界的录像能列出每一档覆盖节目的方式。该处理器刻意独立于最终化它在任何状态下行查找该路径最新的——openSync(wx)在其录像拥有期间保持预留路径排他finalize()从不触碰programs_json两个写入以任意顺序提交。唯一的等待是追踪器队列排空保证行 INSERT 存在——没有截止期也就没有一次性增强被时钟丢掉的可能。停止时若没有播放器挂载在该频道上录像保留其开始快照。录像 IPC 面与 UIrecordings.events.ts暴露RECORDINGS_GET_LIST/GET/STOP/REMOVE/UPDATE_PROGRAMS/REVEAL_FILE/PLAY_FILE加专用RECORDINGS_UPDATE_EVENT裸 ping不与下载共享使录像状态迁移不强制可用性探测的下载重取。活动行用实时fs.stat大小装饰——file_size_bytes只在最终化时持久化管理器里增长的大小来自这里。录像总量同时喂给管理器级 All 芯片与头部活动徽标所以只列录像的页面不会读出 All 0。Reveal/Play 受isManagedRecordingFile门控——路径必须存在于 recordings 表中镜像isManagedDownloadFile使渲染层提供的录像目录保持为写入位置偏好而非 shell 访问授权。RECORDINGS_STOP解析行的session_id并通过EmbeddedMpvNativeService停止使管理器无需了解 MPV 会话即可停止录像——但仅限本进程拥有的行会话 ID 每进程重启分派外部行 ID 会停掉一个无关的本地录像。Remove 保留完成文件在磁盘上与下载同一契约只在没有其他行声称该路径时清理失败行残留的预留——同一时间戳秒内的重试可复用释放的名字。渲染层门控用独立的supportsRecordings能力允许列表——刻意不并入supportsDownloads那会剥夺旧构建整个管理器。UI 侧RecordingsService镜像DownloadsService一个全局列表、经DownloadListLoadState的合并刷新、ping 触发重取。管理器加一个recording过滤芯片recording-manager.viewmodel.ts把行分区为Recording now队列区脉冲 REC 芯片带已用时间与实时文件大小——永远没有百分比长度未知大小来自有界、在飞合并、1 秒截止期的stat死亡网络文件系统上的录像目录降级为无大小而不是卡死所有列表加载、录像专属 Needs attention 列表只有 Remove广播无法重录、以及 16:9 频道标志卡片的Recordings库区recording-queue.component.*、recording-library.component.*。卡片标题用捕获的节目标题回退为频道 开始时间。聚焦详情recording-detail/路由/workspace/downloads/recording/:recordingId经workspace-shell-route.utils.ts隐藏上下文面板与路由搜索显示录制时间范围、跨越 ≥2 档节目时列出的覆盖节目、文件路径与 Play/Reveal/Stop/Remove文件缺失降级为 Back Remove。小结为什么这套架构值得参考IPTVnator 的下载管理器把下载当作一个带持久证明的状态机来建模写前日志表download_archive_finalizations在任何不可逆文件操作之前固化身份证据恢复验证器If-Range / 256 KiB 重叠校验让服务器不配合成为可处理状态而不是错误单活动 FIFO、原子行占用与有界文件系统探测队列把并发冲突面压到最小渲染层坚持主进程是唯一事实来源目录授权、文件存在性、缺失恢复全在主进程判定UI 只做呈现与编排。同时它诚实地标注能力边界——不校验媒体时长、不隔离同用户恶意进程、PWA 端永远拿不到权威列表——这些边界声明本身就是架构文档中信息量最大的部分。后续路线图包括队列重排、批量暂停/取消、磁盘空闲遥测、播放分析以及录像的 frame-copy 截图海报。【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进