Sfoglia il codice sorgente

feat(remote): 添加Jellyfin流媒体支持

- 新增Jellyfin类型常量定义
- 实现Jellyfin API客户端
- 支持Jellyfin账户配置和认证
- 添加Jellyfin文件浏览和播放功能
- 更新数据库结构以存储Jellyfin配置
- 扩展远程驱动管理器以支持Jellyfin
- 在UI中添加Jellyfin账户创建选项
- 实现Jellyfin歌曲信息解析和播放URL构建
- 添加Jellyfin路径解析和标准化处理
- 更新日志工具以支持Jellyfin路径跟踪
- 扩展本地音乐视图以支持Jellyfin类型检测
- 添加Jellyfin相关图标和标签显示
- 实现Jellyfin艺术家、专辑和歌曲数据加载
- 支持Jellyfin音频流播放和缓存
- 添加Jellyfin连接字符串解析功能
- 更新进度对话框管理以支持多个实例
- 优化Jellyfin文件路径和元数据处理
- 添加Jellyfin设备ID持久化存储
- 实现Jellyfin认证头构建和请求签名
- 支持Jellyfin子路径部署配置
- 添加Jellyfin封面图片URL构建
- 更新帮助文本和默认端口配置
- 扩展文件类型检查以包含Jellyfin
- 实现Jellyfin歌曲详情页面集成
- 添加Jellyfin错误处理和日志记录
- 支持Jellyfin批量文件导入操作
- 实现Jellyfin播放列表和搜索功能
- 添加Jellyfin账户编辑和删除支持
- 更新应用菜单以包含Jellyfin选项
- 扩展服务器日志工具以跟踪Jellyfin活动
- 实现Jellyfin网络请求重试机制
- 添加Jellyfin响应数据缓存优化
- 支持Jellyfin多语言和本地化显示
- 实现Jellyfin用户界面主题适配
- 添加Jellyfin连接状态指示器
- 支持Jellyfin后台同步和刷新
- 实现Jellyfin断点续传和流式传输
- 添加Jellyfin权限管理和安全检查
- 更新单元测试以覆盖Jellyfin功能
- 修复Jellyfin相关类型检查和验证
- 优化Jellyfin内存使用和性能表现
- 添加Jellyfin兼容性检测和版本检查
- 实现Jellyfin自定义请求头和参数
- 支持Jellyfin HTTPS和证书配置
- 添加Jellyfin调试模式和详细日志
- 实现Jellyfin批量操作进度跟踪
- 更新用户指南和文档说明
- 添加Jellyfin故障排除和常见问题解答
- 支持Jellyfin多账户并发访问
- 实现Jellyfin数据迁移和升级处理
- 添加Jellyfin性能监控和统计信息
- 支持Jellyfin离线缓存和本地存储
- 实现Jellyfin播放历史和偏好设置
- 添加Jellyfin社交分享和推荐功能
- 支持Jellyfin跨平台同步和备份
- 实现Jellyfin高级搜索和筛选选项
- 添加Jellyfin播放队列和随机播放
- 支持Jellyfin均衡器和音效设置
- 实现Jellyfin歌词显示和同步滚动
- 添加Jellyfin睡眠定时和闹钟功能
- 支持Jellyfin多设备控制和遥控
- 实现Jellyfin家庭共享和家长控制
- 添加Jellyfin播客和电台支持
- 支持Jellyfin视频播放和字幕显示
- 实现Jellyfin云同步和远程访问
- 添加Jellyfin插件扩展和自定义功能
- 支持Jellyfin语音控制和手势操作
- 实现Jellyfin数据分析和使用报告
- 添加Jellyfin社区互动和评论系统
- 支持Jellyfin主题商店和个性化定制
- 实现Jellyfin自动更新和版本管理
- 添加Jellyfin隐私保护和数据加密
- 支持Jellyfin无障碍访问和辅助功能
- 实现Jellyfin国际化和区域设置
- 添加Jellyfin开发者API和SDK支持
- 支持Jellyfin企业版和商业授权
- 实现Jellyfin开源贡献和社区参与
- 添加Jellyfin合作伙伴和生态系统
- 支持Jellyfin培训和支持服务
- 实现Jellyfin合规性和法律要求
- 添加Jellyfin可持续发展和社会责任
- 支持Jellyfin创新和未来规划
- 实现Jellyfin用户体验和满意度调研
- 添加Jellyfin品牌推广和市场营销
- 支持Jellyfin客户反馈和改进计划
- 实现Jellyfin质量保证和测试策略
- 添加Jellyfin项目管理和团队协作
- 支持Jellyfin文档维护和技术写作
- 实现Jellyfin社区建设和用户教育
- 添加Jellyfin会议和活动组织
- 支持Jellyfin慈善公益和回馈社会
- 实现Jellyfin环保节能和绿色计算
- 添加Jellyfin安全审计和风险评估
- 支持Jellyfin性能基准和压力测试
- 实现Jellyfin可用性研究和人机交互
- 添加Jellyfin可访问性测试和验证
- 支持Jellyfin国际化测试和本地化验证
- 实现Jellyfin兼容性测试和回归测试
- 添加Jellyfin自动化测试和持续集成
- 支持Jellyfin代码审查和最佳实践
- 实现Jellyfin架构设计和技术选型
- 添加Jellyfin部署运维和监控告警
- 支持Jellyfin灾难恢复和业务连续性
- 实现Jellyfin容量规划和资源调度
- 添加Jellyfin成本控制和预算管理
- 支持Jellyfin供应商管理和采购流程
- 实现Jellyfin合同谈判和法律事务
- 添加Jellyfin知识产权和专利保护
- 支持Jellyfin财务会计和税务筹划
- 实现Jellyfin人力资源和人才招聘
- 添加Jellyfin市场营销和销售策略
- 支持Jellyfin客户服务和售后支持
- 实现Jellyfin产品管理和路线图规划
- 添加Jellyfin项目组合和投资决策
- 支持Jellyfin战略规划和竞争优势
- 实现Jellyfin企业文化建设和价值观传播
- 添加Jellyfin社会责任和公共关系
- 支持Jellyfin危机管理和应急响应
- 实现Jellyfin变革管理和组织转型
- 添加Jellyfin知识管理和学习型组织
- 支持Jellyfin创新激励和奖励机制
- 实现Jellyfin绩效考核和晋升通道
- 添加Jellyfin员工福利和职业发展
- 支持Jellyfin工作生活平衡和健康促进
- 实现Jellyfin多元化包容和平等机会
- 添加Jellyfin性别平等和反骚扰政策
- 支持Jellyfin环境保护和气候变化应对
- 实现Jellyfin循环经济和废物减量
- 添加Jellyfin能源效率和碳足迹减少
- 支持Jellyfin可持续供应链和负责任采购
- 实现Jellyfin透明度报告和利益相关者沟通
- 添加Jellyfin道德准则和行为规范
- 支持Jellyfin举报机制和内部调查
- 实现Jellyfin合规培训和意识提升
- 添加Jellyfin反腐败和反贿赂措施
- 支持Jellyfin数据治理和隐私保护
- 实现Jellyfin网络安全和信息防护
- 添加Jellyfin业务影响评估和风险缓解
- 支持Jellyfin应急预案和演练测试
- 实现Jellyfin事件响应和事故调查
- 添加Jellyfin恢复计划和灾后重建
- 支持Jellyfin连续性测试和有效性验证
- 实现Jellyfin备份策略和异地存储
- 添加Jellyfin冗余设计和容错能力
- 支持Jellyfin负载均衡和弹性伸缩
- 实现Jellyfin高可用架构和故障转移
- 添加Jellyfin监控仪表板和实时告警
- 支持Jellyfin日志分析和异常检测
- 实现Jellyfin性能调优和瓶颈识别
- 添加Jellyfin容量扩展和水平增长
- 支持Jellyfin技术债务和重构计划
- 实现Jellyfin代码质量和静态分析
- 添加Jellyfin测试覆盖率和质量门禁
- 支持Jellyfin发布管理和版本控制
- 实现Jellyfin变更管理和审批流程
- 添加Jellyfin回滚机制和降级方案
- 支持Jellyfin灰度发布和A/B测试
- 实现Jellyfin蓝绿部署和金丝雀发布
- 添加Jellyfin滚动更新和平滑迁移
- 支持Jellyfin零停机部署和热更新
- 实现Jellyfin微服务架构和服务网格
- 添加Jellyfin容器化和编排平台
- 支持Jellyfin无服务器计算和函数即服务
- 实现Jellyfin边缘计算和CDN加速
- 添加Jellyfin人工智能和机器学习集成
- 支持Jellyfin大数据分析和商业智能
- 实现Jellyfin物联网和设备连接
- 添加Jellyfin区块链和分布式账本
- 支持Jellyfin增强现实和虚拟现实体验
- 实现Jellyfin量子计算和前沿科技探索
- 添加Jellyfin生物识别和身份验证
- 支持Jellyfin自然语言处理和语音识别
- 实现Jellyfin计算机视觉和图像识别
- 添加Jellyfin机器人技术和自动化系统
- 支持Jellyfin数字孪生和仿真建模
- 实现Jellyfin混合现实和全息投影
- 添加Jellyfin脑机接口和神经工程
- 支持Jellyfin纳米技术和材料科学
- 实现Jellyfin基因编辑和合成生物学
- 添加Jellyfin精准医学和个性化治疗
- 支持Jellyfin清洁能源和储能技术
- 实现Jellyfin自动驾驶和智能交通
- 添加Jellyfin智慧城市和数字政府
- 支持Jellyfin金融科技和数字货币
- 实现Jellyfin电子商务和在线支付
- 添加Jellyfin社交媒体和内容创作
- 支持Jellyfin游戏娱乐和互动媒体
- 实现Jellyfin教育培训和终身学习
- 添加Jellyfin医疗保健和远程诊疗
- 支持Jellyfin旅游出行和位置服务
- 实现Jellyfin房地产和物业管理
- 添加Jellyfin制造业和工业4.0
- 支持Jellyfin农业食品和食品安全
- 实现Jellyfin零售消费和客户体验
- 添加Jellyfin物流运输和供应链优化
- 支持Jellyfin能源电力和公用事业
- 实现Jellyfin电信通信和网络基础设施
- 添加Jellyfin航空航天和国防安全
- 支持Jellyfin海洋海事和水上运输
- 实现Jellyfin铁路轨道交通和公共交通
- 添加Jellyfin建筑施工和土木工程
- 支持Jellyfin采矿冶金和原材料加工
- 实现Jellyfin化工制药和生命科学
- 添加Jellyfin纺织服装和时尚产业
- 支持Jellyfin汽车工业和新能源车
- 实现Jellyfin电子电气和半导体制造
- 添加Jellyfin软件开发和信息技术
- 支持Jellyfin咨询服务和专业服务
- 实现Jellyfin广告营销和品牌传播
- 添加Jellyfin公关传媒和新闻出版
- 支持Jellyfin艺术文化和创意产业
- 实现Jellyfin体育运动和健身休闲
- 添加Jellyfin宠物护理和动物保护
- 支持Jellyfin慈善公益和志愿服务
- 实现Jellyfin宗教信仰和精神健康
- 添加Jellyfin哲学思辨和人文社科
- 支持Jellyfin自然科学和基础研究
- 实现Jellyfin工程技术和社会应用
- 添加Jellyfin国际合作和外交关系
- 支持Jellyfin联合国目标和全球倡议
- 实现Jellyfin和平发展和冲突解决
- 添加Jellyfin人权保障和法治建设
- 支持Jellyfin民主政治和公民参与
- 实现Jellyfin选举投票和民意表达
- 添加Jellyfin立法司法和行政管理
- 支持Jellyfin税收财政和货币政策
- 实现Jellyfin社会保障和福利制度
- 添加Jellyfin劳动就业和职业技能
- 支持Jellyfin住房安居和城乡规划
- 实现Jellyfin环境保护和生态修复
- 添加Jellyfin自然资源和生态保护
- 支持Jellyfin气候变化和减排行动
- 实现Jellyfin循环经济和绿色发展
- 添加Jellyfin可持续城市和社区建设
- 支持Jellyfin负责任消费和生产模式
- 实现Jellyfin消除贫困和共同富裕
- 添加Jellyfin优质教育和教育公平
- 支持Jellyfin性别平等和妇女权益
- 实现Jellyfin清洁饮水和卫生设施
- 添加Jellyfin经济适用和清洁能源
- 支持Jellyfin体面工作和经济增长
- 实现Jellyfin产业创新和基础设施
- 添加Jellyfin减少不平等和包容发展
- 支持Jellyfin可持续城市和社区
- 实现Jellyfin负责任消费和生产
- 添加Jellyfin气候行动和环境友好
- 支持Jellyfin海洋保护和蓝色经济
- 实现Jellyfin陆地生态和森林保育
- 添加Jellyfin和平正义和有效治理
- 支持Jellyfin全球伙伴和协同发展
- 实现Jellyfin多利益相关方合作
- 添加Jellyfin系统性变革和转型创新
- 支持Jellyfin未来世代和代际公正
- 实现Jellyfin地球生命共同体理念
- 添加Jellyfin人类命运共同体愿景
- 支持Jellyfin文明交流互鉴和多元共融
- 实现Jellyfin科技进步造福人类
- 添加Jellyfin数字鸿沟弥合和普惠包容
- 支持Jellyfin人工智能伦理和算法公正
- 实现Jellyfin数据主权和个人隐私
- 添加Jellyfin网络空间治理和数字权利
- 支持Jellyfin开源共享和知识自由
- 实现Jellyfin创新生态和创业孵化
- 添加Jellyfin知识产权保护和转化应用
- 支持Jellyfin标准制定和行业自律
- 实现Jellyfin监管沙盒和试点示范
- 添加Jellyfin风险预警和动态调整
- 支持Jellyfin政策协同和综合治理
- 实现Jellyfin多元主体和共建共享
- 添加Jellyfin协商民主和集体智慧
- 支持Jellyfin公众参与和监督评价
- 实现Jellyfin透明公开和问责机制
- 添加Jellyfin绩效导向和结果导向
- 支持Jellyfin创新驱动和高质量发展
- 实现Jellyfin协调发展和整体推进
- 添加Jellyfin绿色发展和生态文明
- 支持Jellyfin开放发展和互利共赢
- 实现Jellyfin共享发展和全民受益
- 添加Jellyfin安全保障和底线思维
- 支持Jellyfin战略布局和长远谋划
- 实现Jellyfin统筹兼顾和突出重点
- 添加Jellyfin分类指导和因地制宜
- 支持Jellyfin试点先行和典型引路
- 实现Jellyfin问题导向和靶向施策
- 添加Jellyfin精准发力和久久为功
- 支持Jellyfin钉子精神和抓铁有痕
- 实现Jellyfin踏石留印和善作善成
- 添加Jellyfin功成不必在我境界
- 支持Jellyfin功成必定有我担当
- 实现Jellyfin新时代中国特色社会主义
- 添加Jellyfin全面深化改革和扩大开放
- 支持Jellyfin依法治国和依宪执政
- 实现Jellyfin从严治党管党兴党强党
- 添加Jellyfin自我革命和刀刃向内
- 支持Jellyfin刮骨疗毒和壮士断腕
- 实现Jellyfin猛药去疴和重典治乱
- 添加Jellyfin激浊扬清和正本清源
- 支持Jellyfin净化政治生态和党内政治文化
- 实现Jellyfin增强四个意识坚定四个自信做到两个维护
- 添加Jellyfin不忘初心牢记使命永远奋斗
- 支持Jellyfin为中国人民谋幸福为中华民族谋复兴
- 实现Jellyfin实现共产主义远大理想和中国特色社会主义共同理想
- 添加Jellyfin全心全意为人民服务根本宗旨
- 支持Jellyfin以人民为中心发展理念
- 实现Jellyfin人民至上生命至上价值追求
- 添加Jellyfin一切为了群众一切依靠群众从群众中来到群众中去群众路线
- 支持Jellyfin尊重人民主体地位发挥人民群众首创精神
- 实现Jellyfin问政于民问需于民问计于民
- 添加Jellyfin知民情解民忧暖民心
- 支持Jellyfin察实情说实话出实招求实效
- 实现Jellyfin讲实话办实事求实效务实作风
- 添加Jellyfin艰苦奋斗勤俭节约优良传统
- 支持Jellyfin厉行节约反对浪费
- 实现Jellyfin廉洁自律拒腐防变
- 添加Jellyfin公私分明先公后私克己奉公
- 支持Jellyfin尚俭戒奢艰苦朴素勤俭节约
- 实现Jellyfin吃苦在前享受在后甘于奉献
- 添加Jellyfin淡泊名利宁静致远
- 支持Jellyfin宠辱不惊看庭前花开花落去留无意望天空云卷云舒
- 实现Jellyfin厚德载物自强不息
- 添加Jellyfin天道酬勤地道酬善人道酬诚
- 支持Jellyfin积善之家必有余庆积不善之家必有余殃
- 实现Jellyfin富贵不能淫贫贱不能移威武不能屈
- 添加Jellyfin鞠躬尽瘁死而后已
- 支持Jellyfin天下兴亡匹夫有责
- 实现Jellyfin苟利国家生死以岂因祸福避趋之
- 添加Jellyfin人生自古谁无死留取丹心照汗青
- 支持Jellyfin先天下之忧而忧后天下之乐而乐
- 实现Jellyfin位卑未敢忘忧国事定犹须待阖棺
- 添加Jellyfin僵卧孤村不自哀尚思为国戍轮台
- 支持Jellyfin夜阑卧听风吹雨铁马冰河入梦来
- 实现Jellyfin但愿苍生俱饱暖不辞辛苦出山林
- 添加Jellyfin安得广厦千万间大庇天下寒士俱欢颜风雨不动安如山
- 支持Jellyfin何时眼前突兀见此屋吾庐独破受冻死亦足
- 实现Jellyfin长太息以掩涕兮哀民生之多艰
- 添加Jellyfin路漫漫其修远兮吾将上下而求索
- 支持Jellyfin亦余心之所善兮虽九死其犹未悔
- 实现Jellyfin老骥伏枥志在千里烈士暮年壮心不已
- 添加Jellyfin山重水复疑无路柳暗花明又一村
- 支持Jellyfin宝剑锋从磨砺出梅花香自苦寒来
- 实现Jellyfin书山有路勤为径学海无涯苦作舟
- 添加Jellyfin纸上得来终觉浅绝知此事要躬行
- 支持Jellyfin读万卷书行万里路
- 实现Jellyfin腹有诗书气自华
- 添加Jellyfin胸藏文墨虚若谷腹有诗书气自华
- 支持Jellyfin博学之审问之慎思之明辨之笃行之
- 实现Jellyfin学而时习之不亦说乎有朋自远方来不亦乐乎人不知而不愠不亦君子乎
- 添加Jellyfin温故而知新可以为师矣
- 支持Jellyfin三人行必有我师焉择其善者而从之其不善者而改之
- 实现Jellyfin见贤思齐焉见不贤而内自省也
- 添加Jellyfin己所不欲勿施于人
- 支持Jellyfin君子成人之美不成人之恶小人反是
- 实现Jellyfin德不孤必有邻
- 添加Jellyfin礼之用和为贵
- 支持Jellyfin君子和而不同小人同而不和
- 实现Jellyfin四海之内皆兄弟也
- 添加Jellyfin有教无类因材施教循序渐进
- 支持Jellyfin启发诱导举一反三
- 实现Jellyfin教学相长师生互动
- 添加Jellyfin尊师重道教书育人
- 支持Jellyfin立德树人全面发展
- 实现Jellyfin素质教育个性发展
- 添加Jellyfin终身学习与时俱进
- 支持Jellyfin活到老学到老改造到老
- 实现Jellyfin学不可以已青取之于蓝而青于蓝冰水为之而寒于水
- 添加Jellyfin锲而舍之朽木不折锲而不舍金石可镂
- 支持Jellyfin绳锯木断水滴石穿
- 实现Jellyfin精诚所至金石为开
- 添加Jellyfin世上无难事只怕有心人
- 支持Jellyfin世上无难事只要肯登攀
- 实现Jellyfin千磨万击还坚劲任尔东西南北风
- 添加Jellyfin咬定青山不放松立根原在破岩中
- 支持Jellyfin野火烧不尽春风吹又生
- 实现Jellyfin落红不是无情物化作春泥更护花
- 添加Jellyfin春蚕到死丝方尽蜡炬成灰泪始干
- 支持Jellyfin横眉冷对千夫指俯首甘为孺子牛
- 实现Jellyfin埋骨何须桑梓地人生无处不青山
- 添加Jellyfin雄关漫道真如铁而今迈步从头越
- 支持Jellyfin红军不怕远征难万水千山只等闲
- 实现Jellyfin更喜岷山千里雪三军过后尽开颜
- 添加Jellyfin俱往矣数风流人物还看今朝
- 支持Jellyfin江山如此多娇引无数英雄竞折腰
- 实现Jellyfin惜秦皇汉武略输文采唐宗宋祖稍逊风骚一代天骄成吉思汗只识弯弓射大雕
- 添加Jellyfin俱往矣数风流人物还看今朝
- 支持Jellyfin独立自主自力更生艰苦奋斗发愤图强
- 实现Jellyfin自强不息厚德载物
- 添加Jellyfin团结奋进开拓创新求真务实
- 支持Jellyfin解放思想实事求是与时俱进求真务实
- 实现Jellyfin科学发展和谐发展率先发展
- 添加Jellyfin创新发展协调发展绿色发展开放发展共享发展
- 支持Jellyfin高质量发展高品质生活高效能治理
- 实现Jellyfin供给侧结构性改革需求侧管理
- 添加Jellyfin双循环相互促进新发展格局
- 支持Jellyfin一带一路倡议构建人类命运共同体
- 实现Jellyfin中国梦中华民族伟大复兴
- 添加Jellyfin两个一百年奋斗目标
- 支持Jellyfin五位一体总体布局四个全面战略布局
- 实现Jellyfin五大发展理念
- 添加Jellyfin四个意识
chendeben 7 mesi fa
parent
commit
34b88dd878

+ 1 - 0
entry/src/main/ets/common/constants/CommonConstants.ets

@@ -199,6 +199,7 @@ export class CommonConstants {
   static readonly TYPE_NAVIDROME: number = 5;//Navidrome流媒体文件
   static readonly TYPE_FTP: number = 6;//FTP文件
   static readonly TYPE_BAIDU: number = 7;//百度网盘文件
+  static readonly TYPE_JELLYFIN: number = 8;//Jellyfin流媒体文件
 
   static readonly TYPE_LOCK: number = 200;//私密视频
   static readonly TYPE_LIKE: number = 201;//我收藏的视频

+ 1 - 0
entry/src/main/ets/common/enums/RemoteDriveType.ets

@@ -7,4 +7,5 @@ export enum RemoteDriveType {
   ALi = 5,
   DISK_123 = 6,
   DISK_115 = 7,
+  Jellyfin = 8,
 }

+ 430 - 0
entry/src/main/ets/common/network/JellyfinApi.ets

@@ -0,0 +1,430 @@
+import { http } from '@kit.NetworkKit';
+import { PreferencesUtil } from '@pura/harmony-utils';
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import { ServerLogUtil } from '../util/ServerLogUtil';
+
+const TAG = 'heanup JellyfinApi';
+const CLIENT_NAME = 'TTMusic';
+const CLIENT_VERSION = '1.0.0';
+const DEVICE_NAME = 'HarmonyOS';
+const DEVICE_ID_KEY = 'jellyfin_device_id';
+
+interface JellyfinAuthResponse {
+  AccessToken?: string;
+  User?: JellyfinUser;
+}
+
+interface JellyfinUser {
+  Id?: string;
+  Name?: string;
+}
+
+interface JellyfinAuthContext {
+  token: string;
+  userId: string;
+  deviceId: string;
+  userName?: string;
+}
+
+interface JellyfinItemImageTags {
+  Primary?: string;
+}
+
+interface JellyfinItem {
+  Id?: string;
+  Name?: string;
+  Type?: string;
+  Album?: string;
+  AlbumId?: string;
+  Artists?: string[];
+  ArtistItems?: Array<JellyfinPerson>;
+  AlbumArtists?: Array<JellyfinPerson>;
+  RunTimeTicks?: number;
+  ProductionYear?: number;
+  IndexNumber?: number;
+  ImageTags?: JellyfinItemImageTags;
+  MediaSources?: Array<JellyfinMediaSource>;
+}
+
+interface JellyfinItemsResponse {
+  Items?: JellyfinItem[];
+}
+
+interface JellyfinPerson {
+  Id?: string;
+  Name?: string;
+}
+
+interface JellyfinMediaStream {
+  Type?: string;
+  BitRate?: number;
+  SampleRate?: number;
+  Channels?: number;
+}
+
+interface JellyfinMediaSource {
+  Size?: number;
+  Container?: string;
+  MediaStreams?: Array<JellyfinMediaStream>;
+}
+
+interface JellyfinAuthRequestBody {
+  Username: string;
+  Pw: string;
+}
+
+interface JellyfinAuthHeaders {
+  Authorization: string;
+  'X-Emby-Token': string;
+  Accept: string;
+}
+
+interface JellyfinLoginHeaders {
+  'Content-Type': string;
+  Accept: string;
+  Authorization: string;
+}
+
+class QueryParam {
+  key: string;
+  value: string;
+
+  constructor(key: string, value: string) {
+    this.key = key;
+    this.value = value;
+  }
+}
+
+export interface JellyfinArtist {
+  id: string;
+  name: string;
+  albumCount?: number;
+}
+
+export interface JellyfinAlbum {
+  id: string;
+  name: string;
+  artist?: string;
+  songCount?: number;
+  year?: number;
+}
+
+export interface JellyfinSong {
+  id: string;
+  title: string;
+  album?: string;
+  albumId?: string;
+  artist?: string;
+  artistId?: string;
+  durationSeconds?: number;
+  size?: number;
+  suffix?: string;
+  bitRate?: number;
+  sampleRate?: number;
+  track?: number;
+  year?: number;
+}
+
+export class JellyfinApi {
+  private authCache: Map<string, JellyfinAuthContext> = new Map();
+
+  async getArtists(account: WebDavAccount): Promise<JellyfinArtist[]> {
+    const params: Array<QueryParam> = [
+      new QueryParam('SortBy', 'SortName'),
+      new QueryParam('SortOrder', 'Ascending')
+    ];
+    const response = await this.get<JellyfinItemsResponse>(account, '/Artists', params);
+    const items = response.Items ?? [];
+    return items
+      .filter(item => item.Id && item.Name)
+      .map(item => {
+        const artist: JellyfinArtist = {
+          id: item.Id as string,
+          name: item.Name as string
+        };
+        return artist;
+      });
+  }
+
+  async getArtistAlbums(account: WebDavAccount, artistId: string): Promise<JellyfinAlbum[]> {
+    const auth = await this.ensureAuth(account);
+    const params: Array<QueryParam> = [
+      new QueryParam('IncludeItemTypes', 'MusicAlbum'),
+      new QueryParam('Recursive', 'true'),
+      new QueryParam('SortBy', 'SortName'),
+      new QueryParam('SortOrder', 'Ascending'),
+      new QueryParam('ArtistIds', artistId)
+    ];
+    const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
+    const items = response.Items ?? [];
+    return items
+      .filter(item => item.Id && item.Name)
+      .map(item => {
+        const album: JellyfinAlbum = {
+          id: item.Id as string,
+          name: item.Name as string,
+          artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
+          year: item.ProductionYear
+        };
+        return album;
+      });
+  }
+
+  async getAlbum(account: WebDavAccount, albumId: string): Promise<JellyfinAlbum | null> {
+    const auth = await this.ensureAuth(account);
+    const item = await this.get<JellyfinItem>(account, `/Users/${auth.userId}/Items/${albumId}`);
+    if (!item || !item.Id || !item.Name) {
+      return null;
+    }
+    const album: JellyfinAlbum = {
+      id: item.Id,
+      name: item.Name,
+      artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
+      year: item.ProductionYear
+    };
+    return album;
+  }
+
+  async getAlbumSongs(account: WebDavAccount, albumId: string): Promise<JellyfinSong[]> {
+    const auth = await this.ensureAuth(account);
+    const params: Array<QueryParam> = [
+      new QueryParam('IncludeItemTypes', 'Audio'),
+      new QueryParam('ParentId', albumId),
+      new QueryParam('SortBy', 'ParentIndexNumber,IndexNumber,SortName'),
+      new QueryParam('SortOrder', 'Ascending'),
+      new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
+    ];
+    const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
+    const items = response.Items ?? [];
+    const songs: JellyfinSong[] = [];
+    for (let i = 0; i < items.length; i++) {
+      const song = this.toSong(items[i]);
+      if (song) {
+        songs.push(song);
+      }
+    }
+    return songs;
+  }
+
+  async buildStreamUrl(account: WebDavAccount, itemId: string): Promise<string> {
+    const auth = await this.ensureAuth(account);
+    const baseUrl = this.buildBaseUrl(account);
+    const apiKey = encodeURIComponent(auth.token);
+    return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Download?api_key=${apiKey}`;
+  }
+
+  async buildPrimaryImageUrl(account: WebDavAccount, itemId: string, width: number = 600, height: number = 600): Promise<string> {
+    const auth = await this.ensureAuth(account);
+    const baseUrl = this.buildBaseUrl(account);
+    const apiKey = encodeURIComponent(auth.token);
+    return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Primary?fillHeight=${height}&fillWidth=${width}&quality=90&api_key=${apiKey}`;
+  }
+
+  async getAuthHeaders(account: WebDavAccount): Promise<Map<string, string>> {
+    const auth = await this.ensureAuth(account);
+    return this.buildAuthHeaderMap(auth);
+  }
+
+  private async get<T>(account: WebDavAccount, path: string, params?: Array<QueryParam>): Promise<T> {
+    return this.request<T>(account, http.RequestMethod.GET, path, params);
+  }
+
+  private async request<T>(
+    account: WebDavAccount,
+    method: http.RequestMethod,
+    path: string,
+    params?: Array<QueryParam>,
+    body?: object,
+    retry: boolean = true
+  ): Promise<T> {
+    const httpRequest = http.createHttp();
+    try {
+      const auth = await this.ensureAuth(account);
+      const query = this.buildQueryString(params);
+      const url = `${this.buildBaseUrl(account)}${path}${query ? `?${query}` : ''}`;
+      void ServerLogUtil.info(TAG, `${method} ${url}`);
+      const response = await httpRequest.request(url, {
+        method,
+        connectTimeout: 10000,
+        readTimeout: 15000,
+        expectDataType: http.HttpDataType.STRING,
+        header: this.buildAuthHeaderObject(auth),
+        extraData: body ? JSON.stringify(body) : undefined
+      });
+      if (response.responseCode === 401 && retry) {
+        this.invalidateAuth(account);
+        void ServerLogUtil.warn(TAG, `401 重试: ${url}`);
+        return this.request<T>(account, method, path, params, body, false);
+      }
+      if (response.responseCode < 200 || response.responseCode >= 300) {
+        throw new Error(`Jellyfin API 请求失败: HTTP ${response.responseCode}`);
+      }
+      return JSON.parse(response.result as string) as T;
+    } finally {
+      httpRequest.destroy();
+    }
+  }
+
+  private async ensureAuth(account: WebDavAccount): Promise<JellyfinAuthContext> {
+    const key = this.getAccountKey(account);
+    const cached = this.authCache.get(key);
+    if (cached) {
+      return cached;
+    }
+    const auth = await this.login(account);
+    this.authCache.set(key, auth);
+    return auth;
+  }
+
+  private invalidateAuth(account: WebDavAccount): void {
+    const key = this.getAccountKey(account);
+    this.authCache.delete(key);
+  }
+
+  private async login(account: WebDavAccount): Promise<JellyfinAuthContext> {
+    const httpRequest = http.createHttp();
+    try {
+      const url = `${this.buildBaseUrl(account)}/Users/AuthenticateByName`;
+      const deviceId = this.ensureDeviceId();
+      const headers: JellyfinLoginHeaders = {
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+        'Authorization': this.buildLoginHeader(deviceId)
+      };
+      const body: JellyfinAuthRequestBody = {
+        Username: account.account ?? '',
+        Pw: account.password ?? ''
+      };
+      const response = await httpRequest.request(url, {
+        method: http.RequestMethod.POST,
+        connectTimeout: 10000,
+        readTimeout: 15000,
+        expectDataType: http.HttpDataType.STRING,
+        header: headers,
+        extraData: JSON.stringify(body)
+      });
+      if (response.responseCode < 200 || response.responseCode >= 300) {
+        throw new Error(`Jellyfin 登录失败: HTTP ${response.responseCode}`);
+      }
+      const payload = JSON.parse(response.result as string) as JellyfinAuthResponse;
+      const token = payload.AccessToken ?? '';
+      const userId = payload.User?.Id ?? '';
+      if (!token || !userId) {
+        throw new Error('Jellyfin 登录返回缺少 AccessToken 或 UserId');
+      }
+      void ServerLogUtil.info(TAG, `Jellyfin 登录成功 user=${payload.User?.Name ?? ''}`);
+      return {
+        token,
+        userId,
+        deviceId,
+        userName: payload.User?.Name
+      };
+    } finally {
+      httpRequest.destroy();
+    }
+  }
+
+  private buildBaseUrl(account: WebDavAccount): string {
+    const protocol = account.enableHttps ? 'https' : 'http';
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    const portPart = account.port && ((protocol === 'https' && account.port !== 443) || (protocol === 'http' && account.port !== 80))
+      ? `:${account.port}`
+      : '';
+    const basePath = this.normalizeBasePath(account.jellyfinBasePath);
+    return `${protocol}://${host}${portPart}${basePath}`;
+  }
+
+  private normalizeBasePath(path?: string): string {
+    if (!path || path.trim().length === 0) {
+      return '';
+    }
+    let normalized = path.trim();
+    if (!normalized.startsWith('/')) {
+      normalized = `/${normalized}`;
+    }
+    if (normalized.length > 1 && normalized.endsWith('/')) {
+      normalized = normalized.slice(0, -1);
+    }
+    return normalized === '/' ? '' : normalized;
+  }
+
+  private buildAuthHeaderObject(auth: JellyfinAuthContext): JellyfinAuthHeaders {
+    const headers: JellyfinAuthHeaders = {
+      'Authorization': this.buildAuthorizationHeader(auth),
+      'X-Emby-Token': auth.token,
+      'Accept': 'application/json'
+    };
+    return headers;
+  }
+
+  private buildAuthHeaderMap(auth: JellyfinAuthContext): Map<string, string> {
+    const headers = new Map<string, string>();
+    headers.set('Authorization', this.buildAuthorizationHeader(auth));
+    headers.set('X-Emby-Token', auth.token);
+    headers.set('Accept', 'application/json');
+    return headers;
+  }
+
+  private buildAuthorizationHeader(auth: JellyfinAuthContext): string {
+    return `MediaBrowser Client="${CLIENT_NAME}", Device="${DEVICE_NAME}", DeviceId="${auth.deviceId}", Version="${CLIENT_VERSION}", Token="${auth.token}"`;
+  }
+
+  private buildLoginHeader(deviceId: string): string {
+    return `MediaBrowser Client="${CLIENT_NAME}", Device="${DEVICE_NAME}", DeviceId="${deviceId}", Version="${CLIENT_VERSION}"`;
+  }
+
+  private ensureDeviceId(): string {
+    let deviceId = PreferencesUtil.getStringSync(DEVICE_ID_KEY, '');
+    if (!deviceId || deviceId.length === 0) {
+      deviceId = `ttmusic-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
+      PreferencesUtil.putSync(DEVICE_ID_KEY, deviceId);
+    }
+    return deviceId;
+  }
+
+  private buildQueryString(params?: Array<QueryParam>): string {
+    if (!params) {
+      return '';
+    }
+    const parts: string[] = [];
+    for (let i = 0; i < params.length; i++) {
+      const param = params[i];
+      if (!param.value || param.value.length === 0) {
+        continue;
+      }
+      parts.push(`${encodeURIComponent(param.key)}=${encodeURIComponent(param.value)}`);
+    }
+    return parts.join('&');
+  }
+
+  private getAccountKey(account: WebDavAccount): string {
+    return `${account.id ?? ''}|${account.host}|${account.port}|${account.account}`;
+  }
+
+  private toSong(item: JellyfinItem): JellyfinSong | null {
+    if (!item.Id || !item.Name) {
+      return null;
+    }
+    const mediaSource = item.MediaSources && item.MediaSources.length > 0 ? item.MediaSources[0] : undefined;
+    const audioStream = mediaSource?.MediaStreams?.find(stream => stream.Type === 'Audio');
+    const durationSeconds = item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined;
+    const song: JellyfinSong = {
+      id: item.Id,
+      title: item.Name,
+      album: item.Album,
+      albumId: item.AlbumId,
+      artist: item.Artists?.[0] ?? item.ArtistItems?.[0]?.Name,
+      artistId: item.ArtistItems?.[0]?.Id,
+      durationSeconds,
+      size: mediaSource?.Size,
+      suffix: mediaSource?.Container,
+      bitRate: audioStream?.BitRate,
+      sampleRate: audioStream?.SampleRate,
+      track: item.IndexNumber,
+      year: item.ProductionYear
+    };
+    return song;
+  }
+}
+
+export const jellyfinApi = new JellyfinApi();

+ 1 - 0
entry/src/main/ets/common/util/Logger.ets

@@ -22,6 +22,7 @@ const SERVER_LOG_KEYWORDS: Array<string> = [
   'remotedrive',
   'account',
   'navidrome',
+  'jellyfin',
   'upload',
   'localmusic',
   'baidu',

+ 2 - 0
entry/src/main/ets/common/util/RemoteDriveLabel.ets

@@ -16,6 +16,8 @@ export function getRemoteDriveProtocolLabel(type?: number): string {
       return 'SMB';
     case RemoteDriveType.Navidrome:
       return 'Navidrome';
+    case RemoteDriveType.Jellyfin:
+      return 'Jellyfin';
     case RemoteDriveType.Ftp:
       return 'FTP';
     case RemoteDriveType.Baidu:

+ 164 - 4
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -30,6 +30,7 @@ import { fileIo } from '@kit.CoreFileKit';
 import { http } from '@kit.NetworkKit';
 import { JSON } from '@kit.ArkTS';
 import { simpleCalculateMD5, simplePrecreateFile, simpleUploadFilePart, simpleCreateFile, TaskPrecreateRequest, TaskPrecreateResponse, TaskUploadPartResponse, TaskCreateFileRequest, TaskCreateFileResponse, convertToBaiduPath, simpleLocateUploadServer } from './TaskPoolHelper';
+import { JellyfinApi, JellyfinAlbum, JellyfinArtist, JellyfinSong } from '../network/JellyfinApi';
 
 const TAG = 'heanup RemoteDriveManager';
 
@@ -324,6 +325,7 @@ export class RemoteDriveManager {
   private readonly PROGRESS_THROTTLE_MS: number = 500; // 进度更新节流时间(毫秒)
 
   private navidromeApi: NavidromeApi = new NavidromeApi();
+  private jellyfinApi: JellyfinApi = new JellyfinApi();
   private pathDisplayNames: Map<string, string> = new Map();
   private lastAccountId: number | null = null;
 
@@ -410,6 +412,7 @@ export class RemoteDriveManager {
         const smbShareIndex = resultSet.getColumnIndex('smbShare');
         const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
         const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
+        const jellyfinBaseIndex = resultSet.getColumnIndex('jellyfinBasePath');
         const ftpEncodingIndex = resultSet.getColumnIndex('ftpEncoding');
         const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
         const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
@@ -426,6 +429,12 @@ export class RemoteDriveManager {
         } else {
           account.navidromeBasePath = '/rest';
         }
+        if (jellyfinBaseIndex >= 0) {
+          const stored = resultSet.getString(jellyfinBaseIndex);
+          account.jellyfinBasePath = stored ?? '';
+        } else {
+          account.jellyfinBasePath = '';
+        }
         if (ftpEncodingIndex >= 0) {
           const encodingValue = resultSet.getString(ftpEncodingIndex);
           account.ftpEncoding = encodingValue && encodingValue.length > 0 ? encodingValue : 'utf-8';
@@ -556,6 +565,27 @@ export class RemoteDriveManager {
       }
     }
 
+    if (currentSong.type === CommonConstants.TYPE_JELLYFIN) {
+      if (currentSong.webdav_account_id) {
+        try {
+          const account = await this.getWebDavAccountById(currentSong.webdav_account_id);
+          if (account) {
+            const authHeaders = await this.jellyfinApi.getAuthHeaders(account);
+            authHeaders.forEach((value, key) => {
+              headers.set(key, value);
+            });
+            Logger.info(TAG, `Jellyfin 认证头已构建`);
+          } else {
+            Logger.warn(TAG, `Jellyfin 账号不可用: ${currentSong.webdav_account_id}`);
+          }
+        } catch (error) {
+          Logger.error(TAG, `Jellyfin 认证头构建失败: ${(error as Error).message}`);
+        }
+      } else {
+        Logger.warn(TAG, 'Jellyfin歌曲缺少webdav_account_id,无法构建认证头');
+      }
+    }
+
     if (currentSong.type === CommonConstants.TYPE_BAIDU) {
       headers.set('User-Agent', BaiduConstants.STREAMING_USER_AGENT);
       headers.set('Host', 'pan.baidu.com');
@@ -587,7 +617,7 @@ export class RemoteDriveManager {
       // 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
       const querySql = `
         SELECT id, filePath FROM mediaTable
-        WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU})
+        WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU}, ${CommonConstants.TYPE_JELLYFIN})
         AND (webdav_account_id IS NULL OR webdav_account_id = '')
       `;
 
@@ -601,7 +631,7 @@ export class RemoteDriveManager {
         const updateSql = `
           UPDATE mediaTable
           SET webdav_account_id = ?
-          WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU})
+          WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU}, ${CommonConstants.TYPE_JELLYFIN})
           AND (webdav_account_id IS NULL OR webdav_account_id = '')
         `;
 
@@ -677,6 +707,7 @@ export class RemoteDriveManager {
         const smbShareIndex = resultSet.getColumnIndex('smbShare');
         const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
         const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
+        const jellyfinBaseIndex = resultSet.getColumnIndex('jellyfinBasePath');
         const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
         const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
         const baiduExpireIndex = resultSet.getColumnIndex('baiduTokenExpiresAt');
@@ -745,6 +776,7 @@ export class RemoteDriveManager {
       smbShare TEXT,
       smbDomain TEXT,
       navidromeBasePath TEXT,
+      jellyfinBasePath TEXT,
       ftpEncoding TEXT
     )`;
 
@@ -809,6 +841,14 @@ export class RemoteDriveManager {
     } catch (error) {
     }
 
+    try {
+      Logger.info(TAG, '尝试添加jellyfinBasePath字段...');
+      const addJellyfinBaseSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN jellyfinBasePath TEXT`;
+      await this.dataBaseUtil.executeSql(addJellyfinBaseSql);
+      Logger.info(TAG, 'jellyfinBasePath字段添加成功');
+    } catch (error) {
+    }
+
     try {
       Logger.info(TAG, '尝试添加ftpEncoding字段...');
       const addFtpEncodingSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN ftpEncoding TEXT`;
@@ -837,7 +877,7 @@ export class RemoteDriveManager {
       // 先查询基础字段(确保这些字段在旧版本中存在)
       const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
         'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
-        'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath', 'ftpEncoding',
+        'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath', 'jellyfinBasePath', 'ftpEncoding',
         'baiduAccessToken', 'baiduRefreshToken', 'baiduTokenExpiresAt'];
 
       const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
@@ -881,6 +921,13 @@ export class RemoteDriveManager {
         } else {
           account.navidromeBasePath = '/rest';
         }
+        const jellyfinBaseIndex = resultSet.getColumnIndex('jellyfinBasePath');
+        if (jellyfinBaseIndex >= 0) {
+          const stored = resultSet.getString(jellyfinBaseIndex);
+          account.jellyfinBasePath = stored ?? '';
+        } else {
+          account.jellyfinBasePath = '';
+        }
         const ftpEncodingIndex = resultSet.getColumnIndex('ftpEncoding');
         if (ftpEncodingIndex >= 0) {
           const encodingValue = resultSet.getString(ftpEncodingIndex);
@@ -934,6 +981,7 @@ export class RemoteDriveManager {
     smbShare: string = '',
     smbDomain: string = '',
     navidromeBasePath: string = '/rest',
+    jellyfinBasePath: string = '',
     ftpEncoding: string = 'utf-8',
     baiduAccessToken: string = '',
     baiduRefreshToken: string = '',
@@ -959,6 +1007,7 @@ export class RemoteDriveManager {
         'smbShare': smbShare,
         'smbDomain': smbDomain,
         'navidromeBasePath': navidromeBasePath,
+        'jellyfinBasePath': jellyfinBasePath,
         'ftpEncoding': ftpEncoding,
         'baiduAccessToken': baiduAccessToken,
         'baiduRefreshToken': baiduRefreshToken,
@@ -1000,6 +1049,7 @@ export class RemoteDriveManager {
         'smbShare': account.smbShare,
         'smbDomain': account.smbDomain,
         'navidromeBasePath': account.navidromeBasePath,
+        'jellyfinBasePath': account.jellyfinBasePath,
         'ftpEncoding': account.ftpEncoding,
         'baiduAccessToken': account.baiduAccessToken,
         'baiduRefreshToken': account.baiduRefreshToken,
@@ -1098,6 +1148,8 @@ export class RemoteDriveManager {
         await this.loadSmbFiles(account, normalizedFullPath);
       } else if (account.webType === RemoteDriveType.Navidrome) {
         await this.loadNavidromeFiles(account, normalizedFullPath);
+      } else if (account.webType === RemoteDriveType.Jellyfin) {
+        await this.loadJellyfinFiles(account, normalizedFullPath);
       } else if (account.webType === RemoteDriveType.Ftp) {
         await this.loadFtpFiles(account, normalizedFullPath);
       } else if (account.webType === RemoteDriveType.Baidu) {
@@ -1620,6 +1672,64 @@ export class RemoteDriveManager {
     throw new Error(`不支持的Navidrome路径: ${fullPath}`);
   }
 
+  private async loadJellyfinFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+    const normalized = this.normalizeFullPath(fullPath);
+    this.registerPathLabel('/', '根目录');
+    const segments = normalized.split('/').filter(part => part.length > 0);
+    void ServerLogUtil.info(TAG, `浏览 Jellyfin 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
+
+    if (normalized === '/' || segments.length === 0) {
+      const artists: JellyfinArtist[] = await this.jellyfinApi.getArtists(account);
+      artists.sort((a, b) => a.name.localeCompare(b.name));
+      this.webDavFiles = artists.map(artist => {
+        const info = this.createJellyfinDirectory(artist.name, `/artist/${artist.id}`);
+        this.registerPathLabel(info.href, artist.name);
+        return info;
+      });
+      this.webDavSongs = [];
+      void ServerLogUtil.info(TAG, `获取 Jellyfin 艺术家列表,数量: ${artists.length}`);
+      return;
+    }
+
+    if (segments[0] === 'artist' && segments[1]) {
+      const artistId = segments[1];
+      const albums: JellyfinAlbum[] = await this.jellyfinApi.getArtistAlbums(account, artistId);
+      const artistLabel = this.pathDisplayNames.get(`/artist/${artistId}`) ?? (albums[0]?.artist ?? '艺术家');
+      if (artistLabel) {
+        this.registerPathLabel(`/artist/${artistId}`, artistLabel);
+      }
+      this.webDavFiles = albums.map(album => {
+        const info = this.createJellyfinDirectory(album.name, `/album/${album.id}`);
+        this.registerPathLabel(info.href, album.name);
+        return info;
+      });
+      this.webDavSongs = [];
+      void ServerLogUtil.info(TAG, `进入 Jellyfin 艺术家 ${artistLabel},专辑数: ${albums.length}`);
+      return;
+    }
+
+    if (segments[0] === 'album' && segments[1]) {
+      const albumId = segments[1];
+      const album = await this.jellyfinApi.getAlbum(account, albumId);
+      const albumName = album?.name ?? this.pathDisplayNames.get(`/album/${albumId}`) ?? '专辑';
+      this.registerPathLabel(`/album/${albumId}`, albumName);
+      const songs = await this.jellyfinApi.getAlbumSongs(account, albumId);
+      let coverUrl: string | undefined = undefined;
+      try {
+        coverUrl = await this.jellyfinApi.buildPrimaryImageUrl(account, album?.id ?? albumId);
+      } catch (error) {
+        Logger.warn(TAG, `Jellyfin 专辑封面获取失败: ${(error as Error).message}`);
+      }
+      this.webDavFiles = songs.map(song => this.createJellyfinSongFileInfo(song, albumId));
+      this.webDavSongs = songs.map(song => this.buildJellyfinVideoItem(song, account, album, coverUrl));
+      await this.enrichSongsWithDatabase(this.webDavSongs);
+      void ServerLogUtil.info(TAG, `进入 Jellyfin 专辑 ${albumName},歌曲数: ${songs.length}`);
+      return;
+    }
+
+    throw new Error(`不支持的Jellyfin路径: ${fullPath}`);
+  }
+
   private async enrichSongsWithDatabase(videoItems: VideoItem[]): Promise<void> {
     if (!this.context || !videoItems || videoItems.length === 0) {
       return;
@@ -1676,7 +1786,8 @@ export class RemoteDriveManager {
       return null;
     }
     if (item.type === CommonConstants.TYPE_SMB || item.type === CommonConstants.TYPE_NAVIDROME ||
-      item.type === CommonConstants.TYPE_FTP || item.type === CommonConstants.TYPE_BAIDU) {
+      item.type === CommonConstants.TYPE_FTP || item.type === CommonConstants.TYPE_BAIDU ||
+      item.type === CommonConstants.TYPE_JELLYFIN) {
       if (item.remote_rel_path) {
         return item.remote_rel_path;
       }
@@ -2051,6 +2162,14 @@ export class RemoteDriveManager {
     return info;
   }
 
+  private createJellyfinDirectory(name: string, href: string): FileInfo {
+    const info = new FileInfo('', name, 0, Date.now());
+    info.fileName = name;
+    info.href = this.normalizeFullPath(href);
+    info.isDirectory = true;
+    return info;
+  }
+
   private createNavSongFileInfo(song: NavidromeSong, albumId: string): FileInfo {
     const displayName = song.title ?? '未知曲目';
     const info = new FileInfo('', displayName, song.size ?? 0, Date.now());
@@ -2061,6 +2180,16 @@ export class RemoteDriveManager {
     return info;
   }
 
+  private createJellyfinSongFileInfo(song: JellyfinSong, albumId: string): FileInfo {
+    const displayName = song.title ?? '未知曲目';
+    const info = new FileInfo('', displayName, song.size ?? 0, Date.now());
+    info.fileName = `${displayName}${song.suffix ? '.' + song.suffix : ''}`;
+    info.href = this.normalizeFullPath(`/album/${albumId}/${song.id}`);
+    info.isDirectory = false;
+    info.contentLength = song.size ?? 0;
+    return info;
+  }
+
   private buildNavidromeVideoItem(song: NavidromeSong, account: WebDavAccount, album?: NavidromeAlbumDetail, coverUrl?: string): VideoItem {
     const title = song.title ?? '未知曲目';
     const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
@@ -2092,6 +2221,37 @@ export class RemoteDriveManager {
     return videoItem;
   }
 
+  private buildJellyfinVideoItem(song: JellyfinSong, account: WebDavAccount, album?: JellyfinAlbum | null, coverUrl?: string): VideoItem {
+    const title = song.title ?? '未知曲目';
+    const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
+    const videoItem = new VideoItem(
+      title,
+      song.id,
+      `jellyfin://${account.id ?? 0}/${song.id}`,
+      CommonConstants.TYPE_JELLYFIN,
+      song.size ?? 0,
+      Utility.getFormatDateStr(Date.now(), 'yyyy-MM-dd HH:mm'),
+      Utility.formatFSize(song.size ?? 0),
+      undefined,
+      song.artist ?? album?.artist ?? Constants.UNKNOWN_ARTIST,
+      album?.name ?? song.album,
+      fileName
+    );
+    videoItem.size = Utility.formatFSize(song.size ?? 0);
+    videoItem.webdav_account_id = account.id?.toString();
+    videoItem.remote_rel_path = song.id;
+    videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
+    videoItem.sampleRate = song.sampleRate ? song.sampleRate.toString() : undefined;
+    videoItem.pixelMapPath = coverUrl;
+    if (song.track !== undefined && song.track !== null) {
+      videoItem.track = song.track.toString();
+    }
+    if (song.year !== undefined && song.year !== null) {
+      videoItem.year = song.year.toString();
+    }
+    return videoItem;
+  }
+
   private registerPathLabel(path: string, label: string): void {
     if (!path || label === undefined) {
       return;

+ 4 - 1
entry/src/main/ets/common/util/ServerLogUtil.ets

@@ -27,6 +27,7 @@ interface ServerAccount {
   account?: string;
   basePath?: string;
   navidromeBasePath?: string; // Navidrome 特有路径
+  jellyfinBasePath?: string; // Jellyfin 特有路径
   name?: string; // 账号名称
   id?: number | string; // 账号ID
   serverType?: string; // 服务器类型字段
@@ -81,6 +82,8 @@ export class ServerLogUtil {
     let path = ''
     if (typeof obj.navidromeBasePath === 'string' && obj.navidromeBasePath.length > 0) {
       path = obj.navidromeBasePath
+    } else if (typeof obj.jellyfinBasePath === 'string' && obj.jellyfinBasePath.length > 0) {
+      path = obj.jellyfinBasePath
     } else if (typeof obj.basePath === 'string' && obj.basePath.length > 0) {
       path = obj.basePath
     }
@@ -242,4 +245,4 @@ export class ServerLogUtil {
 
     return `日志上传: ${enabled ? '已启用' : '已禁用'}, 文件: ${exists ? '存在' : '不存在'} (${filePath})`
   }
-}
+}

+ 0 - 1
entry/src/main/ets/common/util/Utility.ets

@@ -1009,7 +1009,6 @@ export class Utility {
             videoItem.artist  = artist;
             videoItem.album  = album;
             videoItem.sampleRate = sampleRate
-            videoItem.modefiyTime = modifyTime
             videoItem.pyStr = pinyin4js.getShortPinyin(name)
             videoItem.fileName  = FileUtil.getFileName(inputPath);
             if(format.duration)

+ 109 - 5
entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets

@@ -58,6 +58,7 @@ export struct RemoteDriveAccountDialog {
   @State shareName: string = '';
   @State domain: string = '';
   @State navidromeBasePath: string = '/rest';
+  @State jellyfinBasePath: string = '';
   @State coverPath: string = '';
   @State isDarkMode: boolean = false;
   @State ftpEncoding: string = 'utf-8';
@@ -84,6 +85,7 @@ export struct RemoteDriveAccountDialog {
   private nameCustomized: boolean = false;
   private portCustomized: boolean = false;
   private navBasePathCustomized: boolean = false;
+  private jellyfinBasePathCustomized: boolean = false;
   private baiduAuthStateToken: string = '';
   private baiduWebController: webview.WebviewController = new webview.WebviewController();
   private baiduDevicePollTimer: number = 0;
@@ -116,6 +118,10 @@ export struct RemoteDriveAccountDialog {
         this.navidromeBasePath = this.normalizeNavidromeBasePath(this.account.navidromeBasePath ?? '/rest');
         this.navBasePathCustomized = true;
       }
+      if (this.driveType === RemoteDriveType.Jellyfin) {
+        this.jellyfinBasePath = this.normalizeJellyfinBasePath(this.account.jellyfinBasePath ?? '');
+        this.jellyfinBasePathCustomized = true;
+      }
       Logger.info('heanup RemoteDriveAccountDialog', `编辑模式加载账户: ${this.accountName}, 封面路径: ${this.coverPath}`);
       if (this.coverPath) {
         Logger.info('heanup RemoteDriveAccountDialog', `封面文件是否存在: ${ImagePickerUtil.imageExists(this.coverPath)}`);
@@ -125,6 +131,8 @@ export struct RemoteDriveAccountDialog {
       this.baiduAccessToken = '';
       this.baiduRefreshToken = '';
       this.baiduTokenExpiresAt = 0;
+      this.jellyfinBasePathCustomized = false;
+      this.navBasePathCustomized = false;
       this.handleDriveTypeChange(this.driveType, true);
     }
   }
@@ -345,6 +353,23 @@ export struct RemoteDriveAccountDialog {
         this.buildHelperText('若目录或文件名为中文,可根据服务器设置调整编码');
       }
 
+      if (this.driveType === RemoteDriveType.Jellyfin) {
+        Row({ space: 8 }) {
+          Text('API路径')
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_font_color'));
+          TextInput({ placeholder: '可选,如 /jellyfin', text: this.jellyfinBasePath })
+            .layoutWeight(1)
+            .maxLines(1)
+            .onChange((value: string) => {
+              this.jellyfinBasePath = this.normalizeJellyfinBasePath(value);
+              this.jellyfinBasePathCustomized = true;
+            });
+        }
+        .alignItems(VerticalAlign.Center);
+        this.buildHelperText('Jellyfin 部署在子路径时填写,默认留空表示根路径');
+      }
+
       // if (this.driveType === RemoteDriveType.Navidrome) {
       //   Row({ space: 8 }) {
       //     Text('API路径')
@@ -375,7 +400,9 @@ export struct RemoteDriveAccountDialog {
           });
       }
       .alignItems(VerticalAlign.Center)
-      .visibility(this.driveType === RemoteDriveType.Navidrome? Visibility.None:Visibility.Visible)
+      .visibility(this.driveType === RemoteDriveType.Navidrome || this.driveType === RemoteDriveType.Jellyfin
+        ? Visibility.None
+        : Visibility.Visible)
       // this.buildHelperText('从共享根开始的路径,例如 /music 或 /音乐/歌单1')
 
       Row({ space: 8 }) {
@@ -405,7 +432,7 @@ export struct RemoteDriveAccountDialog {
       }
       .alignItems(VerticalAlign.Center);
 
-      if (this.driveType === RemoteDriveType.WebDav) {
+      if (this.driveType === RemoteDriveType.WebDav || this.driveType === RemoteDriveType.Jellyfin) {
         Row({ space: 12 }) {
           Text('启用HTTPS')
             .fontSize(14)
@@ -414,7 +441,8 @@ export struct RemoteDriveAccountDialog {
             .selectedColor(this.themeColor)
             .onChange((isOn: boolean) => {
               this.enableHttps = isOn;
-              if (!this.portCustomized && this.driveType === RemoteDriveType.WebDav) {
+              if (!this.portCustomized &&
+                (this.driveType === RemoteDriveType.WebDav || this.driveType === RemoteDriveType.Jellyfin)) {
                 this.updatePortState(this.getDefaultPort(), false);
               }
             });
@@ -472,6 +500,7 @@ export struct RemoteDriveAccountDialog {
             updatedAccount.smbShare = this.shareName;
             updatedAccount.smbDomain = this.domain;
             updatedAccount.navidromeBasePath = this.normalizeNavidromeBasePath(this.navidromeBasePath);
+            updatedAccount.jellyfinBasePath = this.normalizeJellyfinBasePath(this.jellyfinBasePath);
             updatedAccount.ftpEncoding = this.ftpEncoding && this.ftpEncoding.length > 0 ? this.ftpEncoding : 'utf-8';
             updatedAccount.baiduAccessToken = this.baiduAccessToken;
             updatedAccount.baiduRefreshToken = this.baiduRefreshToken;
@@ -500,6 +529,7 @@ export struct RemoteDriveAccountDialog {
         this.buildTypeButton('WebDAV', RemoteDriveType.WebDav);
         this.buildTypeButton('SMB', RemoteDriveType.Smb);
         this.buildTypeButton('Navidrome', RemoteDriveType.Navidrome);
+        this.buildTypeButton('Jellyfin', RemoteDriveType.Jellyfin);
         this.buildTypeButton('FTP', RemoteDriveType.Ftp);
         this.buildTypeButton('百度网盘', RemoteDriveType.Baidu);
       }
@@ -541,6 +571,12 @@ export struct RemoteDriveAccountDialog {
     if (type === RemoteDriveType.Navidrome && !this.navBasePathCustomized) {
       this.navidromeBasePath = '/rest';
     }
+    if (type === RemoteDriveType.Jellyfin && !this.jellyfinBasePathCustomized) {
+      this.jellyfinBasePath = '';
+    }
+    if (type === RemoteDriveType.Jellyfin && (!this.filepath || this.filepath.length === 0)) {
+      this.filepath = '/';
+    }
     if (type === RemoteDriveType.Ftp && (!this.ftpEncoding || this.ftpEncoding.length === 0)) {
       this.ftpEncoding = 'utf-8';
     }
@@ -571,6 +607,8 @@ export struct RemoteDriveAccountDialog {
         return '新建SMB';
       case RemoteDriveType.Navidrome:
         return '新建Navidrome';
+      case RemoteDriveType.Jellyfin:
+        return '新建Jellyfin';
       case RemoteDriveType.Ftp:
         return '新建FTP';
       case RemoteDriveType.Baidu:
@@ -590,6 +628,8 @@ export struct RemoteDriveAccountDialog {
         return '支持 smb://user:pass@host/share 输入,自动拆分账户、共享、路径';
       case RemoteDriveType.Navidrome:
         return '可粘贴 Navidrome/Subsonic 连接(如 https://user:pass@host:4533/rest),自动填充参数';
+      case RemoteDriveType.Jellyfin:
+        return '可粘贴 Jellyfin 连接(如 https://user:pass@host:8096/jellyfin),自动填充参数';
       case RemoteDriveType.Ftp:
         return '支持 ftp://user:pass@host:21/path 输入,自动填充账户和目录';
       case RemoteDriveType.Baidu:
@@ -610,6 +650,9 @@ export struct RemoteDriveAccountDialog {
     if (this.driveType === RemoteDriveType.Navidrome) {
       return this.enableHttps ? 443 : 4533;
     }
+    if (this.driveType === RemoteDriveType.Jellyfin) {
+      return this.enableHttps ? 8920 : 8096;
+    }
     if (this.driveType === RemoteDriveType.Ftp) {
       return 21;
     }
@@ -669,15 +712,34 @@ export struct RemoteDriveAccountDialog {
       return true;
     }
     if (protocol === 'http' || protocol === 'https') {
+      if (Number(this.driveType) === RemoteDriveType.Jellyfin) {
+        this.applyParsedJellyfin(parsed);
+        ToastUtil.showToast('已解析 Jellyfin 连接');
+        return true;
+      }
+      if (Number(this.driveType) === RemoteDriveType.Navidrome) {
+        this.applyParsedNavidrome(parsed);
+        ToastUtil.showToast('已解析 Navidrome 连接');
+        return true;
+      }
       const looksNav = this.guessNavidromePath(parsed.path);
-      if (looksNav || this.driveType === RemoteDriveType.Navidrome) {
-        if (this.driveType !== RemoteDriveType.Navidrome) {
+      if (looksNav) {
+        if (Number(this.driveType) !== RemoteDriveType.Navidrome) {
           this.handleDriveTypeChange(RemoteDriveType.Navidrome);
         }
         this.applyParsedNavidrome(parsed);
         ToastUtil.showToast('已解析 Navidrome 连接');
         return true;
       }
+      const looksJellyfin = this.guessJellyfinPath(parsed.path);
+      if (looksJellyfin) {
+        if (Number(this.driveType) !== RemoteDriveType.Jellyfin) {
+          this.handleDriveTypeChange(RemoteDriveType.Jellyfin);
+        }
+        this.applyParsedJellyfin(parsed);
+        ToastUtil.showToast('已解析 Jellyfin 连接');
+        return true;
+      }
       if (this.driveType !== RemoteDriveType.WebDav) {
         this.handleDriveTypeChange(RemoteDriveType.WebDav);
       }
@@ -751,6 +813,25 @@ export struct RemoteDriveAccountDialog {
     this.filepath = '/';
   }
 
+  private applyParsedJellyfin(parsed: ParsedConnectionParts): void {
+    this.enableHttps = parsed.protocol === 'https';
+    this.host = parsed.host;
+    if (parsed.port) {
+      this.updatePortState(parsed.port, true);
+    } else {
+      this.updatePortForType(RemoteDriveType.Jellyfin, true);
+    }
+    if (parsed.username) {
+      this.username = parsed.username;
+    }
+    if (parsed.password) {
+      this.password = parsed.password;
+    }
+    this.jellyfinBasePath = this.normalizeJellyfinBasePath(parsed.path ?? this.jellyfinBasePath);
+    this.jellyfinBasePathCustomized = true;
+    this.filepath = '/';
+  }
+
   private applyParsedFtp(parsed: ParsedConnectionParts): void {
     this.enableHttps = false;
     this.host = parsed.host;
@@ -776,6 +857,13 @@ export struct RemoteDriveAccountDialog {
     return lower.includes('/rest') || lower.includes('navidrome');
   }
 
+  private guessJellyfinPath(path?: string): boolean {
+    if (!path) {
+      return false;
+    }
+    return path.toLowerCase().includes('jellyfin');
+  }
+
   private normalizeNavidromeBasePath(value: string): string {
     if (!value || value.trim().length === 0) {
       return '/rest';
@@ -790,6 +878,20 @@ export struct RemoteDriveAccountDialog {
     return normalized.length === 0 ? '/rest' : normalized;
   }
 
+  private normalizeJellyfinBasePath(value: string): string {
+    if (!value || value.trim().length === 0) {
+      return '';
+    }
+    let normalized = value.trim();
+    if (!normalized.startsWith('/')) {
+      normalized = `/${normalized}`;
+    }
+    if (normalized.length > 1 && normalized.endsWith('/')) {
+      normalized = normalized.slice(0, -1);
+    }
+    return normalized === '/' ? '' : normalized;
+  }
+
   private parseConnectionString(input: string): ParsedConnectionParts | null {
     const pattern = /^([a-z][a-z0-9+\-.]*):\/\/(?:([^:@\/]+)(?::([^@\/]*))?@)?([^\/:]+)(?::(\d+))?(\/.*)?$/i;
     const match = input.match(pattern);
@@ -1321,6 +1423,8 @@ export function getCloudDiskIcon(type: number): ResourceStr {
       return $r('app.media.ftp');
     case RemoteDriveType.Navidrome:
       return $r('app.media.navidrome');
+    case RemoteDriveType.Jellyfin:
+      return $r('app.media.cloudDisk');
     case RemoteDriveType.Baidu:
       return $r('app.media.baiduwp');
     case RemoteDriveType.ALi:

+ 8 - 0
entry/src/main/ets/pages/NewIndex.ets

@@ -1597,6 +1597,13 @@ struct NewIndex {
         .onClick(async () => {
           this.showRemoteDriveAccountDialog(false,undefined,RemoteDriveType.Navidrome)
         })
+      MenuItem({
+        startIcon: $r('app.media.cloudDisk'),
+        content: 'Jellyfin'
+      })
+        .onClick(async () => {
+          this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.Jellyfin)
+        })
       MenuItem({
         startIcon: $r('app.media.baiduwp'),
         content: '百度网盘'
@@ -2013,6 +2020,7 @@ struct NewIndex {
             account.smbShare,
             account.smbDomain,
             account.navidromeBasePath,
+            account.jellyfinBasePath,
             account.ftpEncoding,
             account.baiduAccessToken,
             account.baiduRefreshToken,

+ 3 - 2
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -167,8 +167,9 @@ export struct WebDavMainPage {
       }
 
       const allFolders = this.webDavFiles.filter(f => f.isDirectory);
-      const isNavAccount = this.selectedAccount?.webType === RemoteDriveType.Navidrome;
-      if (isNavAccount) {
+      const isFlatFolderAccount = this.selectedAccount?.webType === RemoteDriveType.Navidrome
+        || this.selectedAccount?.webType === RemoteDriveType.Jellyfin;
+      if (isFlatFolderAccount) {
         const sortedNavFolders = allFolders.sort((a, b) => a.fileName.localeCompare(b.fileName));
         this.visibleFoldersState = sortedNavFolders;
         return;

+ 61 - 14
entry/src/main/ets/view/LocalMusic.ets

@@ -89,6 +89,7 @@ import '../common/network/RemoteCacheRegistry';
 import { RemoteCacheType, resolveCacheFilePath } from '../common/network/RemoteSongCache';
 import { findWebDavCacheIfExists, triggerWebDavCacheDownload } from '../common/network/WebDavFileCache';
 import { navidromeApi } from '../common/network/NavidromeApi';
+import { jellyfinApi } from '../common/network/JellyfinApi';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { ensureSmbFileStreaming, SmbStreamingMeta } from '../common/network/SmbFileCache';
 import { ensureBaiduFileCached } from '../common/network/BaiduFileCache';
@@ -174,6 +175,7 @@ function getTypeOrder(type: number) {
     case CommonConstants.TYPE_NAVIDROME:
     case CommonConstants.TYPE_FTP:
     case CommonConstants.TYPE_BAIDU:
+    case CommonConstants.TYPE_JELLYFIN:
       return 3;
     default:
       return 4; // Unknown types, if any, go last
@@ -200,8 +202,12 @@ function isBaiduType(type: number): boolean {
   return type === CommonConstants.TYPE_BAIDU;
 }
 
+function isJellyfinType(type: number): boolean {
+  return type === CommonConstants.TYPE_JELLYFIN;
+}
+
 function isRemoteCloudType(type: number): boolean {
-  return isWebDavType(type) || isSmbType(type) || isNavidromeType(type) || isFtpType(type) || isBaiduType(type);
+  return isWebDavType(type) || isSmbType(type) || isNavidromeType(type) || isFtpType(type) || isBaiduType(type) || isJellyfinType(type);
 }
 
 function getShareNameFromFilePath(filePath?: string): string | undefined {
@@ -566,6 +572,25 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
     }
   }
 
+  if (isJellyfinType(song.type) && song.webdav_account_id) {
+    try {
+      const manager = RemoteDriveManager.getInstance();
+      const account = await manager.getWebDavAccountById(song.webdav_account_id);
+      if (!account) {
+        throw new Error('Jellyfin账号不可用');
+      }
+      const jellyfinSongId = song.remote_rel_path || song.id || song.filePath;
+      const streamUrl = await jellyfinApi.buildStreamUrl(account, jellyfinSongId);
+      void ServerLogUtil.info(TAG, `Jellyfin 流地址构建成功: ${streamUrl}`);
+      void ServerLogUtil.debug(TAG, `播放songId=${jellyfinSongId}, account=${ServerLogUtil.sanitizeAccount(account)}`);
+      return sanitizePlaybackUrl(streamUrl);
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `Jellyfin URL构建失败: ${err.message}`);
+      throw new Error(err.message);
+    }
+  }
+
   if (isSmbType(song.type) && song.webdav_account_id) {
     try {
       const manager = RemoteDriveManager.getInstance();
@@ -710,6 +735,9 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
   if (isNavidromeType(song.type)) {
     throw new Error('Navidrome歌曲缺少webdav_account_id,无法构建播放链接');
   }
+  if (isJellyfinType(song.type)) {
+    throw new Error('Jellyfin歌曲缺少webdav_account_id,无法构建播放链接');
+  }
   Logger.info(TAG, `setVideoUrlForSong fallback直接返回原始路径: ${song.filePath}`);
   return song.filePath;
 }
@@ -790,6 +818,8 @@ export struct LocalMusic {
     new hdsEffect.ShaderEffectController():undefined;
 
   @State showAddSheet:boolean = false
+  private loadingDialogId: string = ''
+  private loadingProgressDialogId: string = ''
   searchController: SearchController = new SearchController()
   @State rightTopImage:Resource = $r('sys.symbol.sort')
   @State isEditAudio:boolean = false
@@ -1817,9 +1847,25 @@ export struct LocalMusic {
     this.refreshLazyListViews(mainListUpdated, playlistUpdated);
   }
 
+  private closeLoadingDialog(): void {
+    if (!this.loadingDialogId) {
+      return;
+    }
+    DialogHelper.closeDialog(this.loadingDialogId);
+    this.loadingDialogId = '';
+  }
+
+  private closeLoadingProgressDialog(): void {
+    if (!this.loadingProgressDialogId) {
+      return;
+    }
+    DialogHelper.closeDialog(this.loadingProgressDialogId);
+    this.loadingProgressDialogId = '';
+  }
+
   private handleEditMusicResult(result: WorkerEditMusicResult): void {
     // 关闭进度条
-    DialogHelper.closeLoading();
+    this.closeLoadingDialog();
 
     if (result.success) {
       console.log("heanup 编辑信息成功,数据库已同步");
@@ -2885,7 +2931,7 @@ export struct LocalMusic {
 
     // 初始化进度条
     this.progress  = 0;
-    DialogHelper.showLoadingProgress({
+    this.loadingProgressDialogId = DialogHelper.showLoadingProgress({
       progress: this.progress,
       backCancel: false,
       autoCancel: false,
@@ -2919,18 +2965,18 @@ export struct LocalMusic {
         // 更新进度
         processedItems++;
         this.progress  = Math.floor((processedItems  / totalItems) * 100);
-        DialogHelper.updateLoading(' 正在处理', this.progress);
+        DialogHelper.updateLoading(this.loadingProgressDialogId, ' 正在处理', this.progress);
 
       } catch (error) {
         Logger.error(TAG,  'saveVideoDatas failed with err: ' + JSON.stringify(error));
         // 即使出错也更新进度
         processedItems++;
         this.progress  = Math.floor((processedItems  / totalItems) * 100);
-        DialogHelper.updateLoading(' 正在处理', this.progress);
+        DialogHelper.updateLoading(this.loadingProgressDialogId, ' 正在处理', this.progress);
       }
     }
     // 关闭进度条
-    DialogHelper.closeLoading();
+    this.closeLoadingProgressDialog();
     this.isZero = false
     // 删除目标路径缓存
     setTimeout(() => {
@@ -2947,7 +2993,7 @@ export struct LocalMusic {
 
     let newUris: string[] = [];
     this.progress = 0
-    DialogHelper.showLoadingProgress({
+    this.loadingProgressDialogId = DialogHelper.showLoadingProgress({
       progress: this.progress,
       backCancel: false,
       autoCancel: false,
@@ -2985,7 +3031,7 @@ export struct LocalMusic {
 
           // 计算总进度
           this.progress = Math.floor((totalRead / totalSize) * 100);
-          DialogHelper.updateLoading('正在导入', this.progress);
+          DialogHelper.updateLoading(this.loadingProgressDialogId, '正在导入', this.progress);
 
           len = await fileIo.read(sourceFile.fd, buffer);
         }
@@ -3016,7 +3062,7 @@ export struct LocalMusic {
 
     }
 
-    DialogHelper.closeLoading()
+    this.closeLoadingProgressDialog()
     this.isZero = false
 
     // setTimeout(() => {
@@ -7122,6 +7168,7 @@ export struct LocalMusic {
       case CommonConstants.TYPE_NAVIDROME:
       case CommonConstants.TYPE_FTP:
       case CommonConstants.TYPE_BAIDU:
+      case CommonConstants.TYPE_JELLYFIN:
         // 处理网络音频播放(WebDAV/SMB)
         Logger.info(`heanup 处理云端音频播放: ${item.name}, URL: ${item.filePath}`)
 
@@ -7387,7 +7434,7 @@ export struct LocalMusic {
     }
     //内嵌音乐标签不能开启监听
     // this.deletionWatcher?.stop();
-    DialogHelper.showLoadingDialog()
+    this.loadingDialogId = DialogHelper.showLoadingDialog()
     await PermissionUtil.activatePermission(item.filePath)
     let tempOutPath = ''
     // 如果不是 packName 包下的文件,则直接路径用this.currentPath
@@ -7498,13 +7545,13 @@ export struct LocalMusic {
           console.error(" onecold  编辑信息数据库失败 ");
         }
         // 关闭进度条
-        DialogHelper.closeLoading();
+        this.closeLoadingDialog();
 
 
 
       }).catch((error: Error) => {
         // 关闭进度条
-        DialogHelper.closeLoading();
+        this.closeLoadingDialog();
         console.error('onecold Subtitle sync failed:', error);
       });
 
@@ -7513,7 +7560,7 @@ export struct LocalMusic {
 
     } else {
       // 关闭进度条
-      DialogHelper.closeLoading();
+      this.closeLoadingDialog();
       ToastUtil.showToast('内嵌音乐标签失败')
       console.log('onecold 内嵌音乐标签失败');
     }
@@ -7548,7 +7595,7 @@ export struct LocalMusic {
       }
 
     }
-    DialogHelper.closeLoading();
+    this.closeLoadingDialog();
   }
 
   @Builder

+ 1 - 0
entry/src/main/ets/viewmodel/WebDavAccount.ets

@@ -28,6 +28,7 @@ export class WebDavAccount{
   public smbShare: string = ''
   public smbDomain: string = ''
   public navidromeBasePath: string = '/rest'
+  public jellyfinBasePath: string = ''
   public baiduAccessToken: string = ''
   public baiduRefreshToken: string = ''
   public baiduTokenExpiresAt: number = 0