├── .gitignore ├── LICENSE ├── README.md ├── icon.png ├── scripts └── douban2trakt.py ├── widgets.fwd └── widgets ├── douban.js ├── live.js ├── trakt.js ├── yatu.js └── zhuijurili.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json 3 | package.json 4 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ForwardWidgets 2 | 3 |

4 | 欢迎使用我的五折码 5 |
6 | 五折码:CHEAP.5 7 |
8 | 七折码:CHEAP 9 |

10 | 11 | ### 一、豆瓣我看&豆瓣个性化推荐 12 | 13 | 14 | 15 | 其中用户ID和Cookie必填 16 | 17 | #### 用户ID获取 18 | de6358e6a8cca3e890f51e5f385e5aaa.png 19 | 我的豆瓣标签页中有显示用户ID 20 | 21 | #### Cookie获取 22 | 最好用iPhone网页登陆豆瓣,然后用Loon或者Qx等工具抓包获取Cookie(不清楚Cookie多久失效,所以可能过一段时间需要重新获取填一次) 23 | 24 | #### 增加豆瓣片单(TMDB版) 25 | 因为type为douban类型的items里依赖豆瓣数据里包含imdb_id,但很多综艺没有设置imdb_id,导致综艺片单识别到的只有个别,所以加了直接查询tmdb的版本 26 | 27 | 豆瓣的综艺title一般都包含第x季字样,所以用replace做了删除操作 28 | 29 | 修复下一页 30 | 31 | 增加doulist支持,并新增下拉选项片单[IMDB MOVIE TOP 250]/[IMDB TV TOP 250]/[意外结局电影] 32 | 33 | #### 增加电影推荐(TMDB版)/剧集推荐(TMDB版) 34 | (说明:由于用title查询TMDB,所以不一定能准确匹配正确影片) 35 | 36 | #### 增加观影偏好(TMDB版) 37 | 又名`找电影/找电视` 38 | 39 | 应网友要求进行搬迁,详情请看issue:https://github.com/huangxd-/ForwardWidgets/issues/6 40 | 41 | ### 二、Trakt我看&Trakt个性化推荐 42 | 43 | ```shell 44 | 几点说明: 45 | 1. trakt.js之所以不用官方trakt api,是因为尝试后发现目前生成的token 24小时之后会过期,如果要继续使用,需用上一次返回的refresh_token重新生成,当前插件也没有好的保存方式 46 | 2. trakt看过里是包含看了一半的电视的,也就是说正在看的电视也会出现在 看过-电视 里 47 | 3. 尝试后发现当前ForwardWidget插件的数据模型里如果type是tmdb,貌似有数据会被缓存覆盖的问题,douban和imdb类型不存在该问题 48 | ``` 49 | 50 | 51 | 52 | 其中用户名和Cookie必填 53 | 54 | #### 用户名获取 55 | 在网页上登录你的Trakt,查看url如下 https://trakt.tv/users/xxxx/watchlist ,其中users后面跟的就是你的用户名,填了用户名后还是获取不到数据的请在Trakt设置里打开隐私开关 56 | 57 | #### Cookie获取 58 | 最好用iPhone网页登陆Trakt,然后用Loon或者Qx等工具抓包获取Cookie(不清楚Cookie多久失效,所以可能过一段时间需要重新获取填一次),一般找_traktsession=xxxx长这样的Cookie 59 | 60 | ### 三、Trakt片单&Trakt追剧日历 61 | `说明:追剧日历只是尝鲜版,j佬到时候应该会在APP内嵌追剧功能 :)` 62 | 63 | 64 | 65 | 其中用户名/片单列表名/Cookie同上,另外追剧日历中新增参数开始日期/天数/排序方式 66 | 67 | > 片单示例 68 | https://trakt.tv/users/giladg/lists/latest-4k-releases?sort=added,asc 69 | 70 | ```shell 71 | 用户名:giladg 72 | 片单列表名:latest-4k-releases 73 | ``` 74 | 75 | #### 排序依据 76 | ```shell 77 | rank:排名算法 78 | added:添加时间 79 | title:标题 80 | released:发布日期 81 | runtime:内容时长 82 | popularity:流行度 83 | random:随机 84 | ``` 85 | 86 | #### 排序方向 87 | asc:正序 or desc:反序 88 | 89 | > Trakt追剧日历会将个人watched, collected, or watchlisted中的节目在日历中呈现,具体可查看 https://trakt.tv/calendars/my/shows-movies 90 | 91 | #### 开始日期 92 | 填数字,0表示今天,-1表示昨天,1表示明天,插件内会自动转换成相应日期 93 | 94 | #### 天数 95 | 填数字,表示从开始日期的n天内的时间区间,最大值33天 96 | 97 | #### 排序方式 98 | 如有时间区间[2025-05-01, 2025-05-07] 99 | 100 | 日期升序:返回 从 2025-05-01 -> 2025-05-07 的节目信息 101 | 102 | 日期降序:返回 从 2025-05-07 -> 2025-05-01 的节目信息 103 | 104 | ### 四、豆瓣想看/已看数据迁移Trakt脚本 105 | 106 | #### 使用教程 107 | `说明:可能有部分会迁移失败` 108 | 1. 先将`douban2trakt.py`脚本中的几个必填参数填上 109 | ```shell 110 | # 豆瓣用户ID 111 | DOUBAN_USER_ID = "" 112 | # TRAKT API APPS的Client ID,请前往 https://trakt.tv/oauth/applications/new 创建 113 | TRAKT_CLIENT_ID = "" 114 | # TRAKT抓包获取的x-csrf-token,需有增删改操作的接口才有 115 | TRAKT_X_CSRF_TOKEN = "" 116 | # TRAKT抓包获取的cookie 117 | TRAKT_COOKIE = "" 118 | ``` 119 | 2. 执行脚本 120 | ```shell 121 | # 迁移想看列表 122 | python douban2trakt.py --type watchlist 123 | # 迁移已看列表和打分,因为豆瓣是5分制,Trakt是10分制,所以会做乘以2操作后打分 124 | python douban2trakt.py --type watched 125 | ``` 126 | 127 | ### 五、直播(电视+网络) 128 | `只能说是尝鲜版,有挺多问题的,看j佬有没有时间优化下 :)` 129 | ```shell 130 | 没有统一ping,不知道哪些会超时,也没预览 131 | 没有分类显示的地方 132 | 小组件已经添加的情况下,没有搜索入口 133 | #有些源播放会闪退,不知道是什么原因,可能是缓冲太大?# 134 | #插件有缓存,如果不清缓存,会老是打开同一个时间点# 135 | ``` 136 | 当前支持直播源内没有自带台标的情况下引用台名对应台标 137 | 138 | 139 | 140 | #### 订阅链接 141 | 内嵌了几个公开源,也可以自定义 142 | 143 | 新增PlutoTV源 144 | ```shell 145 | PlutoTV-阿根廷 (Argentina) 146 | PlutoTV-巴西 (Brazil) 147 | PlutoTV-加拿大 (Canada) 148 | PlutoTV-智利 (Chile) 149 | PlutoTV-德国 (Germany) 150 | PlutoTV-西班牙 (Spain) 151 | PlutoTV-法国 (France) 152 | PlutoTV-英国 (Great Britain) 153 | PlutoTV-意大利 (Italy) 154 | PlutoTV-墨西哥 (Mexico) 155 | PlutoTV-美国 (United States) 156 | ``` 157 | 158 | #### 按组关键字过滤 159 | 一个订阅链接可能有上百上千个频道,可以按组名关键字筛选,至于有哪些组,需要自己打开订阅链接看下 160 | 161 | 增加了正则过滤,如`.*(央视|卫视).*` 162 | 163 | #### 按频道名关键字过滤 164 | 一个订阅链接可能有上百上千个频道,可以按频道名称关键字筛选,至于有哪些频道,需要自己打开订阅链接看下 165 | 166 | 增加了正则过滤,如`.*(B站|虎牙|斗鱼).*` 167 | 168 | ### 六、雅图(每日放送+点播排行榜+评分排行榜) 169 | 170 | 171 | #### 每日放送 172 | `说明:最新的数据只到今天以及前面m天的数据,可以参考官网 http://www.yatu.tv:2082/zuijin.asp ` 173 | ```shell 174 | 类型:动漫/电影/电视剧 175 | 开始日期:n天前,0表示今天,-1表示昨天,以此类推 176 | 天数:从开始日期开始的后面m天的数据 177 | ``` 178 | 179 | #### 点播排行榜 180 | ```shell 181 | 类型:连载动漫/剧场动漫/电影/香港电影/欧美电影/电视剧/美剧/综艺 182 | 时间:今日/本月/历史 183 | ``` 184 | 185 | #### 评分排行榜 186 | ```shell 187 | 类型:动漫/电影/电视剧 188 | 等级:非常好看/好看/一般/烂片 189 | ``` 190 | 191 | ### 七、追剧日历(今/明日播出、各项榜单、今日推荐) 192 | 193 | 194 | #### 今/明日播出 195 | ```shell 196 | 类型:今日播出剧集/今日播出番剧/明日播出剧集/明日播出番剧 197 | ``` 198 | 199 | #### 各项榜单 200 | ```shell 201 | 类型:现正热播/人气 Top 10/新剧雷达/热门国漫/已收官好剧/华语热门/本季新番 202 | 地区:国产剧/日剧/英美剧/番剧/韩剧/港台剧 203 | ``` 204 | 205 | #### 今日推荐 206 | 207 | ### 📈项目 Star 数增长趋势 208 | ## Star History 209 | [![Star History Chart](https://api.star-history.com/svg?repos=huangxd-/ForwardWidgets&type=Date)](https://www.star-history.com/#huangxd-/ForwardWidgets&Date) 210 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangxd-/ForwardWidgets/599c6bfb79277ad83a687fa885f4774de263043f/icon.png -------------------------------------------------------------------------------- /scripts/douban2trakt.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import requests 3 | import time 4 | 5 | # 豆瓣用户ID 6 | DOUBAN_USER_ID = "" 7 | # TRAKT API APPS的Client ID,请前往 https://trakt.tv/oauth/applications/new 创建 8 | TRAKT_CLIENT_ID = "" 9 | # TRAKT抓包获取的x-csrf-token,需有增删改操作的接口才有 10 | TRAKT_X_CSRF_TOKEN = "" 11 | # TRAKT抓包获取的cookie 12 | TRAKT_COOKIE = "" 13 | 14 | 15 | # 获取豆瓣列表 16 | def get_douban(watch_type="done", start=0, count=100): 17 | url = f"https://m.douban.com/rexxar/api/v2/user/{DOUBAN_USER_ID}/interests?status={watch_type}&start={start}&count={count}" 18 | response = requests.get(url, headers={"Referer": "https://m.douban.com/mine/movie", "User-Agent": 19 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}, 20 | verify=False) 21 | response.raise_for_status() 22 | return response.json().get('interests', []) 23 | 24 | 25 | # Trakt搜索函数 26 | def search_trakt(title, douban_year, douban_type): 27 | trakt_type = "movie" if douban_type == "movie" else "show" 28 | url = f"https://api.trakt.tv/search/{trakt_type}?query={title}" 29 | response = requests.get(url, headers={"trakt-api-version": "2", "trakt-api-key": TRAKT_CLIENT_ID, "User-Agent": 30 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}, 31 | verify=False) 32 | if response.status_code == 200 and response.json(): 33 | if douban_year.isdigit(): 34 | year = int(douban_year) 35 | for item in response.json(): 36 | trakt_year = item[trakt_type]["year"] 37 | if not trakt_year: 38 | continue 39 | if (trakt_year - 1) <= year <= (trakt_year + 1): 40 | return item[trakt_type]["ids"]["slug"] 41 | else: 42 | return response.json()[0][trakt_type]["ids"]["slug"] 43 | return None 44 | 45 | 46 | # Trakt标记函数 47 | def mark_trakt(mark_type, slug, douban_type, watched_time): 48 | trakt_type = "movies" if douban_type == "movie" else "shows" 49 | watch_type = "movie" if douban_type == "movie" else "show" 50 | url = f"https://trakt.tv/{trakt_type}/{slug}/{mark_type}" 51 | response = requests.post(url, 52 | headers={"x-csrf-token": TRAKT_X_CSRF_TOKEN, "cookie": TRAKT_COOKIE, "User-Agent": 53 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}, 54 | json={"type": watch_type, "watched_at": watched_time, "collected_at": watched_time}, 55 | verify=False) 56 | if response.status_code == 200 or response.status_code == 201: 57 | print(f"✅ Marked {slug} as {mark_type}.") 58 | else: 59 | print(f"❌ Failed to mark {slug} as {mark_type}: {response.status_code}") 60 | 61 | 62 | # Trakt打分函数 63 | def rate_trakt(stars, slug, douban_type): 64 | trakt_stars = int(stars) * 2 65 | trakt_type = "movies" if douban_type == "movie" else "shows" 66 | watch_type = "movie" if douban_type == "movie" else "show" 67 | url = f"https://trakt.tv/{trakt_type}/{slug}/rate" 68 | response = requests.post(url, 69 | headers={"x-csrf-token": TRAKT_X_CSRF_TOKEN, "cookie": TRAKT_COOKIE, 70 | "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "User-Agent": 71 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", }, 72 | data=f"type={watch_type}&stars={trakt_stars}", 73 | verify=False) 74 | if response.status_code == 200 or response.status_code == 201: 75 | print(f"✅ Stared {slug} {trakt_stars}.") 76 | else: 77 | print(f"❌ Failed to stared {slug}: {response.status_code}") 78 | 79 | 80 | def migrate_douban_to_trakt(): 81 | parser = argparse.ArgumentParser(description="豆瓣想看/已看迁移Trakt脚本,运行前先填写脚本内必要参数") 82 | parser.add_argument( 83 | "-t", 84 | "--type", 85 | choices=["watched", "watchlist"], # 限定只能选这两个值 86 | required=True, # 必填参数 87 | help="迁移类型:watched 或 watchlist" 88 | ) 89 | args = parser.parse_args() 90 | 91 | mark_type = "watch" if args.type == "watched" else "watchlist" 92 | watch_type = "done" if args.type == "watched" else "mark" 93 | print(f"迁移类型: {mark_type}") 94 | 95 | start = 0 96 | count = 50 97 | while True: 98 | douban_list = get_douban(watch_type=watch_type, start=start, count=count) 99 | if not douban_list: 100 | break 101 | for index, item in enumerate(douban_list): 102 | print(f"index: {start + index}") 103 | title = item['subject']['title'] 104 | douban_type = item['subject']['type'] 105 | if douban_type == "book" or douban_type == "music": 106 | continue 107 | douban_year = item['subject']['year'] 108 | watched_time = item['create_time'] 109 | print(f"🔍 Searching Trakt for: {title} {douban_year} {douban_type}") 110 | slug = search_trakt(title, douban_year, douban_type) 111 | if slug: 112 | mark_trakt(mark_type, slug, douban_type, watched_time) 113 | time.sleep(4) # Prevent rate-limiting 114 | if item['rating']: 115 | rate_trakt(item['rating']['value'], slug, douban_type) 116 | time.sleep(4) # Prevent rate-limiting 117 | else: 118 | print(f"❓ Could not find '{title} {douban_year} {douban_type}' on Trakt.") 119 | start += count 120 | 121 | 122 | if __name__ == "__main__": 123 | migrate_douban_to_trakt() 124 | -------------------------------------------------------------------------------- /widgets.fwd: -------------------------------------------------------------------------------- 1 | { 2 | "title": "huangxd's Widgets", 3 | "description": "【五折码:CHEAP.5;七折码:CHEAP】A collection of widgets created by huangxd", 4 | "icon": "https://github.com/huangxd-/ForwardWidgets/raw/main/icon.png", 5 | "widgets": [ 6 | { 7 | "id": "douban", 8 | "title": "豆瓣我看&豆瓣个性化推荐", 9 | "description": "解析豆瓣想看、在看、已看以及根据个人数据生成的个性化推荐【五折码:CHEAP.5;七折码:CHEAP】", 10 | "requiredVersion": "0.0.1", 11 | "version": "1.0.7", 12 | "author": "huangxd", 13 | "url": "https://raw.githubusercontent.com/huangxd-/ForwardWidgets/refs/heads/main/widgets/douban.js" 14 | }, 15 | { 16 | "id": "trakt", 17 | "title": "Trakt我看&Trakt个性化推荐", 18 | "description": "解析Trakt想看、在看、已看、片单、追剧日历以及根据个人数据生成的个性化推荐【五折码:CHEAP.5;七折码:CHEAP】", 19 | "requiredVersion": "0.0.1", 20 | "version": "1.0.9", 21 | "author": "huangxd", 22 | "url": "https://raw.githubusercontent.com/huangxd-/ForwardWidgets/refs/heads/main/widgets/trakt.js" 23 | }, 24 | { 25 | "id": "live", 26 | "title": "直播(电视+网络)", 27 | "description": "解析直播订阅链接【五折码:CHEAP.5;七折码:CHEAP】", 28 | "requiredVersion": "0.0.1", 29 | "version": "1.0.7", 30 | "author": "huangxd", 31 | "url": "https://raw.githubusercontent.com/huangxd-/ForwardWidgets/refs/heads/main/widgets/live.js" 32 | }, 33 | { 34 | "id": "yatu", 35 | "title": "雅图(每日放送+点播排行榜+评分排行榜)", 36 | "description": "解析雅图每日放送更新以及各类排行榜【五折码:CHEAP.5;七折码:CHEAP】", 37 | "requiredVersion": "0.0.1", 38 | "version": "1.0.4", 39 | "author": "huangxd", 40 | "url": "https://raw.githubusercontent.com/huangxd-/ForwardWidgets/refs/heads/main/widgets/yatu.js" 41 | }, 42 | { 43 | "id": "zhuijurili", 44 | "title": "追剧日历(今/明日播出、各项榜单、今日推荐)", 45 | "description": "解析追剧日历今/明日播出剧集/番剧/国漫/综艺、各项榜单、今日推荐等【五折码:CHEAP.5;七折码:CHEAP】", 46 | "requiredVersion": "0.0.1", 47 | "version": "1.0.2", 48 | "author": "huangxd", 49 | "url": "https://raw.githubusercontent.com/huangxd-/ForwardWidgets/refs/heads/main/widgets/zhuijurili.js" 50 | }, 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /widgets/douban.js: -------------------------------------------------------------------------------- 1 | // 豆瓣片单组件 2 | WidgetMetadata = { 3 | id: "douban", 4 | title: "豆瓣我看&豆瓣个性化推荐", 5 | modules: [ 6 | { 7 | title: "豆瓣我看", 8 | requiresWebView: false, 9 | functionName: "loadInterestItems", 10 | params: [ 11 | { 12 | name: "user_id", 13 | title: "用户ID", 14 | type: "input", 15 | description: "未填写情况下接口不可用", 16 | }, 17 | { 18 | name: "status", 19 | title: "状态", 20 | type: "enumeration", 21 | enumOptions: [ 22 | { 23 | title: "想看", 24 | value: "mark", 25 | }, 26 | { 27 | title: "在看", 28 | value: "doing", 29 | }, 30 | { 31 | title: "看过", 32 | value: "done", 33 | }, 34 | ], 35 | }, 36 | { 37 | name: "page", 38 | title: "页码", 39 | type: "page" 40 | }, 41 | ], 42 | }, 43 | { 44 | title: "豆瓣个性化推荐", 45 | requiresWebView: false, 46 | functionName: "loadSuggestionItems", 47 | params: [ 48 | { 49 | name: "cookie", 50 | title: "用户Cookie", 51 | type: "input", 52 | description: "未填写情况下非个性化推荐;可手机登陆网页版后,通过loon,Qx等软件抓包获取Cookie", 53 | }, 54 | { 55 | name: "type", 56 | title: "类型", 57 | type: "enumeration", 58 | enumOptions: [ 59 | { 60 | title: "电影", 61 | value: "movie", 62 | }, 63 | { 64 | title: "电视", 65 | value: "tv", 66 | }, 67 | ], 68 | }, 69 | { 70 | name: "page", 71 | title: "页码", 72 | type: "page" 73 | }, 74 | ], 75 | }, 76 | { 77 | title: "豆瓣片单(TMDB版)", 78 | requiresWebView: false, 79 | functionName: "loadCardItems", 80 | params: [ 81 | { 82 | name: "url", 83 | title: "列表地址", 84 | type: "input", 85 | description: "豆瓣片单地址", 86 | placeholders: [ 87 | { 88 | title: "豆瓣热门电影", 89 | value: "https://m.douban.com/subject_collection/movie_hot_gaia", 90 | }, 91 | { 92 | title: "热播新剧", 93 | value: "https://m.douban.com/subject_collection/tv_hot", 94 | }, 95 | { 96 | title: "热播综艺", 97 | value: "https://m.douban.com/subject_collection/show_hot", 98 | }, 99 | { 100 | title: "影院热映", 101 | value: "https://m.douban.com/subject_collection/movie_showing", 102 | }, 103 | { 104 | title: "实时热门电影", 105 | value: "https://m.douban.com/subject_collection/movie_real_time_hotest", 106 | }, 107 | { 108 | title: "实时热门电视", 109 | value: "https://m.douban.com/subject_collection/tv_real_time_hotest", 110 | }, 111 | { 112 | title: "豆瓣 Top 250", 113 | value: "https://m.douban.com/subject_collection/movie_top250", 114 | }, 115 | { 116 | title: "一周电影口碑榜", 117 | value: "https://m.douban.com/subject_collection/movie_weekly_best", 118 | }, 119 | { 120 | title: "华语口碑剧集榜", 121 | value: "https://m.douban.com/subject_collection/tv_chinese_best_weekly", 122 | }, 123 | { 124 | title: "全球口碑剧集榜", 125 | value: "https://m.douban.com/subject_collection/tv_global_best_weekly", 126 | }, 127 | { 128 | title: "国内综艺口碑榜", 129 | value: "https://m.douban.com/subject_collection/show_chinese_best_weekly", 130 | }, 131 | { 132 | title: "全球综艺口碑榜", 133 | value: "https://m.douban.com/subject_collection/show_global_best_weekly", 134 | }, 135 | { 136 | title: "第97届奥斯卡", 137 | value: "https://m.douban.com/subject_collection/EC7I7ZDRA?type=rank", 138 | }, 139 | { 140 | title: "IMDB MOVIE TOP 250", 141 | value: "https://m.douban.com/doulist/1518184", 142 | }, 143 | { 144 | title: "IMDB TV TOP 250", 145 | value: "https://m.douban.com/doulist/41573512", 146 | }, 147 | { 148 | title: "意外结局电影", 149 | value: "https://m.douban.com/doulist/11324", 150 | }, 151 | ], 152 | }, 153 | { 154 | name: "page", 155 | title: "页码", 156 | type: "page" 157 | }, 158 | ], 159 | }, 160 | { 161 | title: "电影推荐(TMDB版)", 162 | requiresWebView: false, 163 | functionName: "loadRecommendMovies", 164 | params: [ 165 | { 166 | name: "category", 167 | title: "分类", 168 | type: "enumeration", 169 | enumOptions: [ 170 | { 171 | title: "全部", 172 | value: "all", 173 | }, 174 | { 175 | title: "热门电影", 176 | value: "热门", 177 | }, 178 | { 179 | title: "最新电影", 180 | value: "最新", 181 | }, 182 | { 183 | title: "豆瓣高分", 184 | value: "豆瓣高分", 185 | }, 186 | { 187 | title: "冷门佳片", 188 | value: "冷门佳片", 189 | }, 190 | ], 191 | }, 192 | { 193 | name: "type", 194 | title: "类型", 195 | type: "enumeration", 196 | belongTo: { 197 | paramName: "category", 198 | value: ["热门", "最新", "豆瓣高分", "冷门佳片"], 199 | }, 200 | enumOptions: [ 201 | { 202 | title: "全部", 203 | value: "全部", 204 | }, 205 | { 206 | title: "华语", 207 | value: "华语", 208 | }, 209 | { 210 | title: "欧美", 211 | value: "欧美", 212 | }, 213 | { 214 | title: "韩国", 215 | value: "韩国", 216 | }, 217 | { 218 | title: "日本", 219 | value: "日本", 220 | }, 221 | ], 222 | }, 223 | { 224 | name: "page", 225 | title: "页码", 226 | type: "page" 227 | }, 228 | ], 229 | }, 230 | { 231 | title: "剧集推荐(TMDB版)", 232 | requiresWebView: false, 233 | functionName: "loadRecommendShows", 234 | params: [ 235 | { 236 | name: "category", 237 | title: "分类", 238 | type: "enumeration", 239 | enumOptions: [ 240 | { 241 | title: "全部", 242 | value: "all", 243 | }, 244 | { 245 | title: "热门剧集", 246 | value: "tv", 247 | }, 248 | { 249 | title: "热门综艺", 250 | value: "show", 251 | }, 252 | ], 253 | }, 254 | { 255 | name: "type", 256 | title: "类型", 257 | type: "enumeration", 258 | belongTo: { 259 | paramName: "category", 260 | value: ["tv"], 261 | }, 262 | enumOptions: [ 263 | { 264 | title: "综合", 265 | value: "tv", 266 | }, 267 | { 268 | title: "国产剧", 269 | value: "tv_domestic", 270 | }, 271 | { 272 | title: "欧美剧", 273 | value: "tv_american", 274 | }, 275 | { 276 | title: "日剧", 277 | value: "tv_japanese", 278 | }, 279 | { 280 | title: "韩剧", 281 | value: "tv_korean", 282 | }, 283 | { 284 | title: "动画", 285 | value: "tv_animation", 286 | }, 287 | { 288 | title: "纪录片", 289 | value: "tv_documentary", 290 | }, 291 | ], 292 | }, 293 | { 294 | name: "type", 295 | title: "类型", 296 | type: "enumeration", 297 | belongTo: { 298 | paramName: "category", 299 | value: ["show"], 300 | }, 301 | enumOptions: [ 302 | { 303 | title: "综合", 304 | value: "show", 305 | }, 306 | { 307 | title: "国内", 308 | value: "show_domestic", 309 | }, 310 | { 311 | title: "国外", 312 | value: "show_foreign", 313 | }, 314 | ], 315 | }, 316 | { 317 | name: "page", 318 | title: "页码", 319 | type: "page" 320 | }, 321 | ], 322 | }, 323 | { 324 | title: "观影偏好(TMDB版)", 325 | description: "根据个人偏好推荐影视作品", 326 | requiresWebView: false, 327 | functionName: "getPreferenceRecommendations", 328 | params: [ 329 | { 330 | name: "mediaType", 331 | title: "类别", 332 | type: "enumeration", 333 | value: "movie", 334 | enumOptions: [ 335 | { title: "电影", value: "movie" }, 336 | { title: "剧集", value: "tv" }, 337 | ] 338 | }, 339 | { 340 | name: "movieGenre", 341 | title: "类型", 342 | type: "enumeration", 343 | belongTo: { 344 | paramName: "mediaType", 345 | value: ["movie"], 346 | }, 347 | enumOptions: [ 348 | { title: "全部", value: "" }, 349 | { title: "喜剧", value: "喜剧" }, 350 | { title: "爱情", value: "爱情" }, 351 | { title: "动作", value: "动作" }, 352 | { title: "科幻", value: "科幻" }, 353 | { title: "动画", value: "动画" }, 354 | { title: "悬疑", value: "悬疑" }, 355 | { title: "犯罪", value: "犯罪" }, 356 | { title: "音乐", value: "音乐" }, 357 | { title: "历史", value: "历史" }, 358 | { title: "奇幻", value: "奇幻" }, 359 | { title: "恐怖", value: "恐怖" }, 360 | { title: "战争", value: "战争" }, 361 | { title: "西部", value: "西部" }, 362 | { title: "歌舞", value: "歌舞" }, 363 | { title: "传记", value: "传记" }, 364 | { title: "武侠", value: "武侠" }, 365 | { title: "纪录片", value: "纪录片" }, 366 | { title: "短片", value: "短片" }, 367 | ] 368 | }, 369 | { 370 | name: "tvModus", 371 | title: "形式", 372 | type: "enumeration", 373 | belongTo: { 374 | paramName: "mediaType", 375 | value: ["tv"], 376 | }, 377 | enumOptions: [ 378 | { title: "全部", value: "" }, 379 | { title: "电视剧", value: "电视剧" }, 380 | { title: "综艺", value: "综艺" }, 381 | ] 382 | }, 383 | { 384 | name: "tvGenre", 385 | title: "类型", 386 | type: "enumeration", 387 | belongTo: { 388 | paramName: "tvModus", 389 | value: ["电视剧"], 390 | }, 391 | enumOptions: [ 392 | { title: "全部", value: "" }, 393 | { title: "喜剧", value: "喜剧" }, 394 | { title: "爱情", value: "爱情" }, 395 | { title: "悬疑", value: "悬疑" }, 396 | { title: "动画", value: "动画" }, 397 | { title: "武侠", value: "武侠" }, 398 | { title: "古装", value: "古装" }, 399 | { title: "家庭", value: "家庭" }, 400 | { title: "犯罪", value: "犯罪" }, 401 | { title: "科幻", value: "科幻" }, 402 | { title: "恐怖", value: "恐怖" }, 403 | { title: "历史", value: "历史" }, 404 | { title: "战争", value: "战争" }, 405 | { title: "动作", value: "动作" }, 406 | { title: "冒险", value: "冒险" }, 407 | { title: "传记", value: "传记" }, 408 | { title: "剧情", value: "剧情" }, 409 | { title: "奇幻", value: "奇幻" }, 410 | { title: "惊悚", value: "惊悚" }, 411 | { title: "灾难", value: "灾难" }, 412 | { title: "歌舞", value: "歌舞" }, 413 | { title: "音乐", value: "音乐" }, 414 | ] 415 | }, 416 | { 417 | name: "zyGenre", 418 | title: "类型", 419 | type: "enumeration", 420 | belongTo: { 421 | paramName: "tvModus", 422 | value: ["综艺"], 423 | }, 424 | enumOptions: [ 425 | { title: "全部", value: "" }, 426 | { title: "真人秀", value: "真人秀" }, 427 | { title: "脱口秀", value: "脱口秀" }, 428 | { title: "音乐", value: "音乐" }, 429 | { title: "歌舞", value: "歌舞" }, 430 | ] 431 | }, 432 | { 433 | name: "region", 434 | title: "地区", 435 | type: "enumeration", 436 | enumOptions: [ 437 | { title: "全部地区", value: "" }, 438 | { title: "华语", value: "华语" }, 439 | { title: "欧美", value: "欧美" }, 440 | { title: "韩国", value: "韩国" }, 441 | { title: "日本", value: "日本" }, 442 | { title: "中国大陆", value: "中国大陆" }, 443 | { title: "中国香港", value: "中国香港" }, 444 | { title: "中国台湾", value: "中国台湾" }, 445 | { title: "美国", value: "美国" }, 446 | { title: "英国", value: "英国" }, 447 | { title: "法国", value: "法国" }, 448 | { title: "德国", value: "德国" }, 449 | { title: "意大利", value: "意大利" }, 450 | { title: "西班牙", value: "西班牙" }, 451 | { title: "印度", value: "印度" }, 452 | { title: "泰国", value: "泰国" } 453 | ] 454 | }, 455 | { 456 | name: "year", 457 | title: "年份", 458 | type: "enumeration", 459 | enumOptions: [ 460 | { title: "全部年份", value: "" }, 461 | { title: "2025", value: "2025" }, 462 | { title: "2024", value: "2024" }, 463 | { title: "2023", value: "2023" }, 464 | { title: "2022", value: "2022" }, 465 | { title: "2021", value: "2021" }, 466 | { title: "2020年代", value: "2020年代" }, 467 | { title: "2010年代", value: "2010年代" }, 468 | { title: "2000年代", value: "2000年代" }, 469 | { title: "90年代", value: "90年代" }, 470 | { title: "80年代", value: "80年代" }, 471 | { title: "70年代", value: "70年代" }, 472 | { title: "60年代", value: "60年代" }, 473 | { title: "更早", value: "更早" }, 474 | ] 475 | }, 476 | { 477 | name: "platform", 478 | title: "平台", 479 | type: "enumeration", 480 | belongTo: { 481 | paramName: "mediaType", 482 | value: ["tv"], 483 | }, 484 | enumOptions: [ 485 | { title: "全部", value: "" }, 486 | { title: "腾讯视频", value: "腾讯视频" }, 487 | { title: "爱奇艺", value: "爱奇艺" }, 488 | { title: "优酷", value: "优酷" }, 489 | { title: "湖南卫视", value: "湖南卫视" }, 490 | { title: "Netflix", value: "Netflix" }, 491 | { title: "HBO", value: "HBO" }, 492 | { title: "BBC", value: "BBC" }, 493 | { title: "NHK", value: "NHK" }, 494 | { title: "CBS", value: "CBS" }, 495 | { title: "NBC", value: "NBC" }, 496 | { title: "tvN", value: "tvN" }, 497 | ], 498 | }, 499 | { 500 | name: "sort_by", 501 | title: "排序", 502 | type: "enumeration", 503 | enumOptions: [ 504 | { title: "综合排序", value: "T" }, 505 | { title: "近期热度", value: "U" }, 506 | { title: "首映时间", value: "R" }, 507 | { title: "高分优选", value: "S" } 508 | ] 509 | }, 510 | { 511 | name: "tags", 512 | title: "自定义标签", 513 | type: "input", 514 | description: "设置自定义标签,例如:丧尸,推理", 515 | value: "", 516 | placeholders: [ 517 | { 518 | title: "空", 519 | value: "", 520 | }, 521 | { 522 | title: "推理,悬疑", 523 | value: "推理,悬疑", 524 | }, 525 | { 526 | title: "cult", 527 | value: "cult", 528 | }, 529 | { 530 | title: "经典", 531 | value: "经典", 532 | }, 533 | { 534 | title: "动作", 535 | value: "动作", 536 | }, 537 | { 538 | title: "喜剧", 539 | value: "喜剧", 540 | }, 541 | { 542 | title: "惊悚", 543 | value: "惊悚", 544 | }, 545 | { 546 | title: "穿越", 547 | value: "穿越", 548 | }, 549 | { 550 | title: "儿童", 551 | value: "儿童", 552 | }, 553 | { 554 | title: "战争", 555 | value: "战争", 556 | }, 557 | ] 558 | }, 559 | { 560 | name: "rating", 561 | title: "评分", 562 | type: "input", 563 | description: "设置最低评分过滤,例如:6", 564 | placeholders: [ 565 | { 566 | title: "0", 567 | value: "0", 568 | }, 569 | { 570 | title: "1", 571 | value: "1", 572 | }, 573 | { 574 | title: "2", 575 | value: "2", 576 | }, 577 | { 578 | title: "3", 579 | value: "3", 580 | }, 581 | { 582 | title: "4", 583 | value: "4", 584 | }, 585 | { 586 | title: "5", 587 | value: "5", 588 | }, 589 | { 590 | title: "6", 591 | value: "6", 592 | }, 593 | { 594 | title: "7", 595 | value: "7", 596 | }, 597 | { 598 | title: "8", 599 | value: "8", 600 | }, 601 | { 602 | title: "9", 603 | value: "9", 604 | }, 605 | ] 606 | }, 607 | { 608 | name: "offset", 609 | title: "起始位置", 610 | type: "offset" 611 | } 612 | ] 613 | }, 614 | ], 615 | version: "1.0.7", 616 | requiredVersion: "0.0.1", 617 | description: "解析豆瓣想看、在看、已看以及根据个人数据生成的个性化推荐【五折码:CHEAP.5;七折码:CHEAP】", 618 | author: "huangxd", 619 | site: "https://github.com/huangxd-/ForwardWidgets" 620 | }; 621 | 622 | async function loadInterestItems(params = {}) { 623 | const page = params.page; 624 | const user_id = params.user_id || ""; 625 | const status = params.status || ""; 626 | const count = 20 627 | start = (page - 1) * count 628 | let url = `https://m.douban.com/rexxar/api/v2/user/${user_id}/interests?status=${status}&start=${start}&count=${count}`; 629 | const response = await Widget.http.get(url, { 630 | headers: { 631 | Referer: `https://m.douban.com/mine/movie`, 632 | "User-Agent": 633 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 634 | }, 635 | }); 636 | 637 | console.log("请求结果:", response.data); 638 | if (response.data && response.data.interests) { 639 | const items = response.data.interests; 640 | const doubanIds = [...new Set( 641 | items 642 | .filter((item) => item.subject.id != null) 643 | .map((item) => item.subject.id) 644 | )].map((id) => ({ 645 | id, 646 | type: "douban", 647 | })); 648 | return doubanIds; 649 | } 650 | return []; 651 | } 652 | 653 | async function loadSuggestionItems(params = {}) { 654 | const page = params.page; 655 | const cookie = params.cookie || ""; 656 | const type = params.type || ""; 657 | const count = 20 658 | const start = (page - 1) * count 659 | const ckMatch = cookie.match(/ck=([^;]+)/); 660 | const ckValue = ckMatch ? ckMatch[1] : null; 661 | let url = `https://m.douban.com/rexxar/api/v2/${type}/suggestion?start=${start}&count=${count}&new_struct=1&with_review=1&ck=${ckValue}`; 662 | const response = await Widget.http.get(url, { 663 | headers: { 664 | Referer: `https://m.douban.com/movie`, 665 | Cookie: cookie, 666 | "User-Agent": 667 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 668 | }, 669 | }); 670 | 671 | console.log("请求结果:", response.data); 672 | if (response.data && response.data.items) { 673 | const items = response.data.items; 674 | const doubanIds = items.filter((item) => item.id != null).map((item) => ({ 675 | id: item.id, 676 | type: "douban", 677 | })); 678 | return doubanIds; 679 | } 680 | return []; 681 | } 682 | 683 | // 基础获取TMDB数据方法 684 | async function fetchTmdbData(key, mediaType) { 685 | const tmdbResults = await Widget.tmdb.get(`/search/${mediaType}`, { 686 | params: { 687 | query: key, 688 | language: "zh_CN", 689 | } 690 | }); 691 | //打印结果 692 | // console.log("搜索内容:" + key) 693 | if (!tmdbResults) { 694 | return []; 695 | } 696 | console.log("tmdbResults:" + JSON.stringify(tmdbResults, null, 2)); 697 | // console.log("tmdbResults.total_results:" + tmdbResults.total_results); 698 | // console.log("tmdbResults.results[0]:" + tmdbResults.results[0]); 699 | return tmdbResults.results; 700 | } 701 | 702 | async function fetchImdbItems(scItems) { 703 | const promises = scItems.map(async (scItem) => { 704 | // 模拟API请求 705 | if (!scItem || !scItem.title) { 706 | return null; 707 | } 708 | const title = scItem.title.replace(/ 第[^季]*季/, ''); 709 | console.log("title: ", title, " ; type: ", scItem.type); 710 | const tmdbDatas = await fetchTmdbData(title, scItem.type) 711 | 712 | if (tmdbDatas.length !== 0) { 713 | return { 714 | id: tmdbDatas[0].id, 715 | type: "tmdb", 716 | title: tmdbDatas[0].title ?? tmdbDatas[0].name, 717 | description: tmdbDatas[0].overview, 718 | releaseDate: tmdbDatas[0].release_date ?? tmdbDatas[0].first_air_date, 719 | backdropPath: tmdbDatas[0].backdrop_path, 720 | posterPath: tmdbDatas[0].poster_path, 721 | rating: tmdbDatas[0].vote_average, 722 | mediaType: scItem.type !== "multi" ? scItem.type : tmdbDatas[0].media_type, 723 | }; 724 | } else { 725 | return null; 726 | } 727 | }); 728 | 729 | // 等待所有请求完成 730 | const items = (await Promise.all(promises)).filter(Boolean); 731 | 732 | // 去重:保留第一次出现的 title 733 | const seenTitles = new Set(); 734 | const uniqueItems = items.filter((item) => { 735 | if (seenTitles.has(item.title)) { 736 | return false; 737 | } 738 | seenTitles.add(item.title); 739 | return true; 740 | }); 741 | 742 | return uniqueItems; 743 | } 744 | 745 | // 解析豆瓣片单 746 | async function loadCardItems(params = {}) { 747 | try { 748 | console.log("开始解析豆瓣片单..."); 749 | console.log("参数:", params); 750 | // 获取片单 URL 751 | const url = params.url; 752 | if (!url) { 753 | console.error("缺少片单 URL"); 754 | throw new Error("缺少片单 URL"); 755 | } 756 | // 验证 URL 格式 757 | if (url.includes("douban.com/doulist/")) { 758 | return loadDefaultList(params); 759 | } else if (url.includes("douban.com/subject_collection/")) { 760 | return loadSubjectCollection(params); 761 | } 762 | } catch (error) { 763 | console.error("解析豆瓣片单失败:", error); 764 | throw error; 765 | } 766 | } 767 | 768 | async function loadDefaultList(params = {}) { 769 | const url = params.url; 770 | // 提取片单 ID 771 | const listId = url.match(/doulist\/(\d+)/)?.[1]; 772 | console.debug("片单 ID:", listId); 773 | if (!listId) { 774 | console.error("无法获取片单 ID"); 775 | throw new Error("无法获取片单 ID"); 776 | } 777 | 778 | const page = params.page; 779 | const count = 25 780 | const start = (page - 1) * count 781 | // 构建片单页面 URL 782 | const pageUrl = `https://www.douban.com/doulist/${listId}/?start=${start}&sort=seq&playable=0&sub_type=`; 783 | 784 | console.log("请求片单页面:", pageUrl); 785 | // 发送请求获取片单页面 786 | const response = await Widget.http.get(pageUrl, { 787 | headers: { 788 | Referer: `https://movie.douban.com/explore`, 789 | "User-Agent": 790 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 791 | }, 792 | }); 793 | 794 | if (!response || !response.data) { 795 | throw new Error("获取片单数据失败"); 796 | } 797 | 798 | console.log("片单页面数据长度:", response.data.length); 799 | console.log("开始解析"); 800 | 801 | // 解析 HTML 得到文档 ID 802 | const docId = Widget.dom.parse(response.data); 803 | if (docId < 0) { 804 | throw new Error("解析 HTML 失败"); 805 | } 806 | console.log("解析成功:", docId); 807 | 808 | // 获取所有视频项,得到元素ID数组 809 | const videoElementIds = Widget.dom.select(docId, ".doulist-item .title a"); 810 | 811 | console.log("items:", videoElementIds); 812 | 813 | let doubanIds = []; 814 | for (const itemId of videoElementIds) { 815 | const link = await Widget.dom.attr(itemId, "href"); 816 | // 获取元素文本内容并分割 817 | const text = await Widget.dom.text(itemId); 818 | // 按空格分割文本并取第一部分 819 | const chineseTitle = text.trim().split(' ')[0]; 820 | if (chineseTitle) { 821 | doubanIds.push({ title: chineseTitle, type: "multi" }); 822 | } 823 | } 824 | 825 | const items = await fetchImdbItems(doubanIds); 826 | 827 | console.log(items) 828 | 829 | return items; 830 | } 831 | 832 | async function loadItemsFromApi(params = {}) { 833 | const url = params.url; 834 | console.log("请求 API 页面:", url); 835 | const listId = params.url.match(/subject_collection\/(\w+)/)?.[1]; 836 | const response = await Widget.http.get(url, { 837 | headers: { 838 | Referer: `https://m.douban.com/subject_collection/${listId}/`, 839 | "User-Agent": 840 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 841 | }, 842 | }); 843 | 844 | console.log("请求结果:", response.data); 845 | if (response.data && response.data.subject_collection_items) { 846 | const scItems = response.data.subject_collection_items; 847 | 848 | const items = await fetchImdbItems(scItems); 849 | 850 | console.log(items) 851 | 852 | return items; 853 | } 854 | return []; 855 | } 856 | 857 | async function loadSubjectCollection(params = {}) { 858 | const listId = params.url.match(/subject_collection\/(\w+)/)?.[1]; 859 | console.debug("合集 ID:", listId); 860 | if (!listId) { 861 | console.error("无法获取合集 ID"); 862 | throw new Error("无法获取合集 ID"); 863 | } 864 | 865 | const page = params.page; 866 | const count = 20 867 | const start = (page - 1) * count 868 | let pageUrl = `https://m.douban.com/rexxar/api/v2/subject_collection/${listId}/items?start=${start}&count=${count}&updated_at&items_only=1&type_tag&for_mobile=1`; 869 | if (params.type) { 870 | pageUrl += `&type=${params.type}`; 871 | } 872 | params.url = pageUrl; 873 | return await loadItemsFromApi(params); 874 | } 875 | 876 | async function loadRecommendMovies(params = {}) { 877 | return await loadRecommendItems(params, "movie"); 878 | } 879 | 880 | async function loadRecommendShows(params = {}) { 881 | return await loadRecommendItems(params, "tv"); 882 | } 883 | 884 | async function loadRecommendItems(params = {}, type = "movie") { 885 | const page = params.page; 886 | const count = 20 887 | const start = (page - 1) * count 888 | const category = params.category || ""; 889 | const categoryType = params.type || ""; 890 | let url = `https://m.douban.com/rexxar/api/v2/subject/recent_hot/${type}?start=${start}&limit=${count}&category=${category}&type=${categoryType}`; 891 | if (category == "all") { 892 | url = `https://m.douban.com/rexxar/api/v2/${type}/recommend?refresh=0&start=${start}&count=${count}&selected_categories=%7B%7D&uncollect=false&score_range=0,10&tags=`; 893 | } 894 | const response = await Widget.http.get(url, { 895 | headers: { 896 | Referer: `https://movie.douban.com/${type}`, 897 | "User-Agent": 898 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 899 | }, 900 | }); 901 | 902 | console.log("请求结果:", response.data); 903 | if (response.data && response.data.items) { 904 | const recItems = response.data.items; 905 | 906 | const items = await fetchImdbItems(recItems); 907 | 908 | console.log(items) 909 | 910 | return items; 911 | } 912 | return []; 913 | } 914 | 915 | // 观影偏好 916 | async function getPreferenceRecommendations(params = {}) { 917 | try { 918 | const rating = params.rating || "0"; 919 | if (!/^\d$/.test(String(rating))) throw new Error("评分必须为 0~9 的整数"); 920 | 921 | const selectedCategories = { 922 | "类型": params.movieGenre || params.tvGenre || params.zyGenre || "", 923 | "地区": params.region || "", 924 | "形式": params.tvModus || "", 925 | }; 926 | console.log("selectedCategories: ", selectedCategories); 927 | 928 | const tags_sub = []; 929 | if (params.movieGenre) tags_sub.push(params.movieGenre); 930 | if (params.tvModus && !params.tvGenre && !params.zyGenre) tags_sub.push(params.tvModus); 931 | if (params.tvModus && params.tvGenre) tags_sub.push(params.tvGenre); 932 | if (params.tvModus && params.zyGenre) tags_sub.push(params.zyGenre); 933 | if (params.region) tags_sub.push(params.region); 934 | if (params.year) tags_sub.push(params.year); 935 | if (params.platform) tags_sub.push(params.platform); 936 | if (params.tags) { 937 | const customTagsArray = params.tags.split(',').filter(tag => tag.trim() !== ''); 938 | tags_sub.push(...customTagsArray); 939 | } 940 | console.log("tags_sub: ", tags_sub); 941 | 942 | const limit = 20; 943 | const offset = Number(params.offset); 944 | const url = `https://m.douban.com/rexxar/api/v2/${params.mediaType}/recommend?refresh=0&start=${offset}&count=${Number(offset) + limit}&selected_categories=${encodeURIComponent(JSON.stringify(selectedCategories))}&uncollect=false&score_range=${rating},10&tags=${encodeURIComponent(tags_sub.join(","))}&sort=${params.sort_by}`; 945 | 946 | const response = await Widget.http.get(url, { 947 | headers: { 948 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 949 | "Referer": "https://movie.douban.com/explore" 950 | } 951 | }); 952 | 953 | if (!response.data?.items?.length) throw new Error("未找到匹配的影视作品"); 954 | 955 | const validItems = response.data.items.filter(item => item.card === "subject"); 956 | 957 | if (!validItems.length) throw new Error("未找到有效的影视作品"); 958 | 959 | const items = await fetchImdbItems(validItems); 960 | 961 | console.log(items) 962 | 963 | return items; 964 | } catch (error) { 965 | throw error; 966 | } 967 | } 968 | -------------------------------------------------------------------------------- /widgets/live.js: -------------------------------------------------------------------------------- 1 | // 电视直播插件 2 | WidgetMetadata = { 3 | id: "live", 4 | title: "直播(电视+网络)", 5 | modules: [ 6 | { 7 | title: "直播(电视+网络)", 8 | requiresWebView: false, 9 | functionName: "loadLiveItems", 10 | params: [ 11 | { 12 | name: "url", 13 | title: "订阅链接", 14 | type: "input", 15 | description: "输入直播订阅链接地址", 16 | placeholders: [ 17 | { 18 | title: "Kimentanm", 19 | value: "https://raw.githubusercontent.com/Kimentanm/aptv/master/m3u/iptv.m3u" 20 | }, 21 | { 22 | title: "网络直播", 23 | value: "https://tv.iill.top/m3u/Live" 24 | }, 25 | { 26 | title: "smart(港澳台)", 27 | value: "https://smart.pendy.dpdns.org/m3u/merged_judy.m3u" 28 | }, 29 | { 30 | title: "YanG-Gather1", 31 | value: "https://tv.iill.top/m3u/Gather" 32 | }, 33 | { 34 | title: "YanG-Gather2", 35 | value: "https://raw.githubusercontent.com/YanG-1989/m3u/main/Gather.m3u" 36 | }, 37 | { 38 | title: "suxuang", 39 | value: "https://bit.ly/suxuang-v4" 40 | }, 41 | { 42 | title: "feiyang", 43 | value: "https://feiyang.wangdu.site/ALL-Sub.m3u" 44 | }, 45 | { 46 | title: "PlutoTV-美国", 47 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_US.m3u" 48 | }, 49 | { 50 | title: "PlutoTV-墨西哥", 51 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_MX.m3u" 52 | }, 53 | { 54 | title: "PlutoTV-意大利", 55 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_IT.m3u" 56 | }, 57 | { 58 | title: "PlutoTV-英国", 59 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_GB.m3u" 60 | }, 61 | { 62 | title: "PlutoTV-法国", 63 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_FR.m3u" 64 | }, 65 | { 66 | title: "PlutoTV-西班牙", 67 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_ES.m3u" 68 | }, 69 | { 70 | title: "PlutoTV-德国", 71 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_DE.m3u" 72 | }, 73 | { 74 | title: "PlutoTV-智利", 75 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_CL.m3u" 76 | }, 77 | { 78 | title: "PlutoTV-加拿大", 79 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_CA.m3u" 80 | }, 81 | { 82 | title: "PlutoTV-巴西", 83 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_BR.m3u" 84 | }, 85 | { 86 | title: "PlutoTV-阿根廷", 87 | value: "https://raw.githubusercontent.com/HelmerLuzo/PlutoTV_HL/refs/heads/main/tv/m3u/PlutoTV_tv_AR.m3u" 88 | }, 89 | { 90 | title: "IPTV1", 91 | value: "https://raw.githubusercontent.com/skddyj/iptv/main/IPTV.m3u" 92 | }, 93 | { 94 | title: "IPTV2-CN", 95 | value: "https://iptv-org.github.io/iptv/countries/cn.m3u" 96 | }, 97 | { 98 | title: "IPTV3", 99 | value: "https://cdn.jsdelivr.net/gh/Guovin/iptv-api@gd/output/result.m3u" 100 | }, 101 | ] 102 | }, 103 | { 104 | name: "group_filter", 105 | title: "按组关键字过滤(选填),如央视,会筛选出所有group-title中包含央视的频道", 106 | type: "input", 107 | description: "输入组关键字,如央视,会筛选出所有group-title中包含央视的频道", 108 | placeholders: [ 109 | { 110 | title: "全部", 111 | value: "", 112 | }, 113 | { 114 | title: "央视&卫视", 115 | value: ".*(央视|卫视).*", 116 | }, 117 | { 118 | title: "央视", 119 | value: "央视", 120 | }, 121 | { 122 | title: "卫视", 123 | value: "卫视", 124 | }, 125 | ] 126 | }, 127 | { 128 | name: "name_filter", 129 | title: "按频道名关键字过滤(选填),如卫视,会筛选出所有频道名中包含卫视的频道", 130 | type: "input", 131 | description: "输入频道名关键字过滤(选填),如卫视,会筛选出所有频道名中包含卫视的频道", 132 | placeholders: [ 133 | { 134 | title: "全部", 135 | value: "", 136 | }, 137 | { 138 | title: "B站&虎牙&斗鱼", 139 | value: ".*(B站|虎牙|斗鱼).*", 140 | }, 141 | { 142 | title: "英雄联盟", 143 | value: "英雄联盟", 144 | }, 145 | { 146 | title: "王者荣耀", 147 | value: "王者荣耀", 148 | }, 149 | { 150 | title: "绝地求生", 151 | value: "绝地求生", 152 | }, 153 | { 154 | title: "和平精英", 155 | value: "和平精英", 156 | }, 157 | ] 158 | }, 159 | { 160 | name: "bg_color", 161 | title: "台标背景色(只对源里不自带台标的起作用)", 162 | type: "input", 163 | description: "支持RGB颜色,如DCDCDC", 164 | value: "DCDCDC", 165 | placeholders: [ 166 | { 167 | title: "亮灰色", 168 | value: "DCDCDC", 169 | }, 170 | { 171 | title: "钢蓝", 172 | value: "4682B4", 173 | }, 174 | { 175 | title: "浅海洋蓝", 176 | value: "20B2AA", 177 | }, 178 | { 179 | title: "浅粉红", 180 | value: "FFB6C1", 181 | }, 182 | { 183 | title: "小麦色", 184 | value: "F5DEB3", 185 | }, 186 | ] 187 | }, 188 | { 189 | name: "direction", 190 | title: "台标优先显示方向", 191 | type: "enumeration", 192 | description: "台标优先显示方向,默认为竖向", 193 | value: "V", 194 | enumOptions: [ 195 | {title: "竖向", value: "V"}, 196 | {title: "横向", value: "H"}, 197 | ] 198 | }, 199 | ], 200 | }, 201 | ], 202 | version: "1.0.7", 203 | requiredVersion: "0.0.1", 204 | description: "解析直播订阅链接【五折码:CHEAP.5;七折码:CHEAP】", 205 | author: "huangxd", 206 | site: "https://github.com/huangxd-/ForwardWidgets" 207 | }; 208 | 209 | 210 | async function loadLiveItems(params = {}) { 211 | try { 212 | const url = params.url || ""; 213 | const groupFilter = params.group_filter || ""; 214 | const nameFilter = params.name_filter || ""; 215 | const bgColor = params.bg_color || ""; 216 | const direction = params.direction || ""; 217 | 218 | if (!url) { 219 | throw new Error("必须提供电视直播订阅链接"); 220 | } 221 | 222 | // 从URL获取M3U内容 223 | const response = await this.fetchM3UContent(url); 224 | if (!response) return []; 225 | 226 | // 获取台标数据 227 | const iconList = await this.fetchIconList(url); 228 | 229 | // 解析M3U内容 230 | const items = parseM3UContent(response, iconList, bgColor, direction); 231 | 232 | // 应用过滤器 233 | const filteredItems = items.filter(item => { 234 | // 组过滤(支持正则表达式) 235 | const groupMatch = !groupFilter || (() => { 236 | try { 237 | // 尝试将输入作为正则表达式解析 238 | const regex = new RegExp(groupFilter, 'i'); 239 | return regex.test(item.metadata?.group || ''); 240 | } catch (e) { 241 | // 若解析失败则回退到普通字符串包含检查(大小写无关) 242 | return (item.metadata?.group?.toLowerCase() || '').includes(groupFilter.toLowerCase()); 243 | } 244 | })(); 245 | 246 | // 名称过滤(支持正则表达式) 247 | const nameMatch = !nameFilter || (() => { 248 | try { 249 | // 尝试将输入作为正则表达式解析 250 | const regex = new RegExp(nameFilter, 'i'); 251 | return regex.test(item.title || ''); 252 | } catch (e) { 253 | // 若解析失败则回退到普通字符串包含检查(大小写无关) 254 | return (item.title?.toLowerCase() || '').includes(nameFilter.toLowerCase()); 255 | } 256 | })(); 257 | 258 | // 只有当两个条件都满足时才返回 true 259 | return groupMatch && nameMatch; 260 | }); 261 | 262 | // 获取过滤后的总数 263 | const totalCount = filteredItems.length; 264 | 265 | // 为每个频道的标题添加 (x/y) 标记 266 | return filteredItems.map((item, index) => ({ 267 | ...item, 268 | title: `${item.title} (${index + 1}/${totalCount})` 269 | })); 270 | } catch (error) { 271 | console.error(`解析电视直播链接时出错: ${error.message}`); 272 | return []; 273 | } 274 | } 275 | 276 | 277 | async function fetchM3UContent(url) { 278 | try { 279 | const response = await Widget.http.get(url, { 280 | headers: { 281 | 'User-Agent': 'AptvPlayer/1.4.6', 282 | } 283 | }); 284 | 285 | console.log("请求结果:", response.data); 286 | 287 | if (response.data && response.data.includes("#EXTINF")) { 288 | return response.data; 289 | } 290 | 291 | return null; 292 | } catch (error) { 293 | console.error(`获取M3U内容时出错: ${error.message}`); 294 | return null; 295 | } 296 | } 297 | 298 | 299 | async function fetchIconList() { 300 | try { 301 | const response = await Widget.http.get("https://api.github.com/repos/fanmingming/live/contents/tv", { 302 | headers: { 303 | 'Accept': 'application/vnd.github.v3+json', 304 | } 305 | }); 306 | 307 | console.log("请求结果:", response.data); 308 | 309 | const iconList = response.data.map(item => item.name.replace('.png', '')); 310 | 311 | console.log("iconList:", iconList); // ["4K电影"] 312 | 313 | return iconList; 314 | } catch (error) { 315 | console.error(`获取台标数据时出错: ${error.message}`); 316 | return []; 317 | } 318 | } 319 | 320 | 321 | function parseM3UContent(content, iconList, bgColor, direction) { 322 | if (!content || !content.trim()) return []; 323 | 324 | const lines = content.split(/\r?\n/); 325 | const items = []; 326 | let currentItem = null; 327 | 328 | // 正则表达式用于匹配M3U标签和属性 329 | const extInfRegex = /^#EXTINF:(-?\d+)(.*),(.*)$/; 330 | const groupRegex = /group-title="([^"]+)"/; 331 | const tvgNameRegex = /tvg-name="([^"]+)"/; 332 | const tvgLogoRegex = /tvg-logo="([^"]+)"/; 333 | const tvgIdRegex = /tvg-id="([^"]+)"/; 334 | 335 | for (let i = 0; i < lines.length; i++) { 336 | const line = lines[i].trim(); 337 | 338 | // 跳过空行和注释行 339 | if (!line || line.startsWith('#EXTM3U')) continue; 340 | 341 | // 匹配#EXTINF行 342 | if (line.startsWith('#EXTINF:')) { 343 | const match = line.match(extInfRegex); 344 | if (match) { 345 | const duration = match[1]; 346 | const attributes = match[2]; 347 | const title = match[3].trim(); 348 | 349 | // 提取属性 350 | const groupMatch = attributes.match(groupRegex); 351 | const tvgNameMatch = attributes.match(tvgNameRegex); 352 | const tvgLogoMatch = attributes.match(tvgLogoRegex); 353 | const tvgIdMatch = attributes.match(tvgIdRegex); 354 | 355 | const group = groupMatch ? groupMatch[1] : '未分类'; 356 | const tvgName = tvgNameMatch ? tvgNameMatch[1] : title; 357 | const cover = tvgLogoMatch ? tvgLogoMatch[1] : ''; 358 | const tvgId = tvgIdMatch ? tvgIdMatch[1] : ''; 359 | 360 | // 创建新的直播项目 361 | currentItem = { 362 | duration, 363 | title, 364 | group, 365 | tvgName, 366 | tvgId, 367 | cover, 368 | url: null 369 | }; 370 | } 371 | } 372 | // 匹配直播URL行 373 | else if (currentItem && line && !line.startsWith('#')) { 374 | const url = line; 375 | console.log(currentItem.title); 376 | // const icon = iconList.includes(currentItem.title) 377 | // ? `https://live.fanmingming.cn/tv/${currentItem.title}.png` 378 | // : ""; 379 | if (!bgColor) { 380 | bgColor = "DCDCDC"; 381 | } 382 | const posterIcon = iconList.includes(currentItem.title) 383 | ? `https://ik.imagekit.io/huangxd/tr:l-image,i-transparent.png,w-bw_mul_3.5,h-bh_mul_3,bg-${bgColor},lfo-center,l-image,i-${currentItem.title}.png,lfo-center,l-end,l-end/${currentItem.title}.png` 384 | : ""; 385 | console.log("posterIcon:", posterIcon); 386 | const backdropIcon = iconList.includes(currentItem.title) 387 | ? `https://ik.imagekit.io/huangxd/tr:l-image,i-transparent.png,w-bw_mul_1.5,h-bh_mul_4,bg-${bgColor},lfo-center,l-image,i-${currentItem.title}.png,lfo-center,l-end,l-end/${currentItem.title}.png` 388 | : ""; 389 | console.log("backdropIcon:", backdropIcon); 390 | 391 | // 构建最终的项目对象 392 | const item = { 393 | id: url, 394 | type: "url", 395 | title: currentItem.title, 396 | // posterPath: posterIcon || currentItem.cover || "https://i.miji.bid/2025/05/17/343e3416757775e312197588340fc0d3.png", 397 | backdropPath: backdropIcon || currentItem.cover || "https://i.miji.bid/2025/05/17/c4a0703b68a4d2313a27937d82b72b6a.png", 398 | previewUrl: "", // 直播通常没有预览URL 399 | link: url, 400 | // 额外的元数据 401 | metadata: { 402 | group: currentItem.group, 403 | tvgName: currentItem.tvgName, 404 | tvgId: currentItem.tvgId 405 | } 406 | }; 407 | if (!direction || direction === "V") { 408 | item['posterPath'] = posterIcon || currentItem.cover || "https://i.miji.bid/2025/05/17/343e3416757775e312197588340fc0d3.png"; 409 | } 410 | 411 | items.push(item); 412 | currentItem = null; // 重置当前项目 413 | } 414 | } 415 | 416 | return items; 417 | } 418 | 419 | 420 | async function loadDetail(link) { 421 | let videoUrl = link; 422 | let childItems = [] 423 | 424 | const formats = ['m3u8', 'mp4', 'mp3', 'flv', 'avi', 'mov', 'wmv', 'webm', 'ogg', 'mkv', 'ts']; 425 | if (!formats.some(format => link.includes(format))) { 426 | // 获取重定向location 427 | const url = `https://redirect-check.hxd.ip-ddns.com/redirect-check?url=${link}`; 428 | 429 | const response = await Widget.http.get(url, { 430 | headers: { 431 | "User-Agent": "AptvPlayer/1.4.6", 432 | }, 433 | }); 434 | 435 | console.log(response.data) 436 | 437 | if (response.data && response.data.location && formats.some(format => response.data.location.includes(format))) { 438 | videoUrl = response.data.location; 439 | } 440 | 441 | if (response.data && response.data.error && response.data.error.includes("超时")) { 442 | const hint_item = { 443 | id: videoUrl, 444 | type: "url", 445 | title: "超时/上面直播不可用", 446 | posterPath: "https://i.miji.bid/2025/05/17/561121fb0ba6071d4070627d187b668b.png", 447 | backdropPath: "https://i.miji.bid/2025/05/17/561121fb0ba6071d4070627d187b668b.png", 448 | link: videoUrl, 449 | }; 450 | childItems = [hint_item] 451 | } 452 | } 453 | 454 | const item = { 455 | id: link, 456 | type: "detail", 457 | videoUrl: videoUrl, 458 | customHeaders: { 459 | "Referer": link, 460 | "User-Agent": "AptvPlayer/1.4.6", 461 | }, 462 | childItems: childItems, 463 | }; 464 | 465 | return item; 466 | } 467 | -------------------------------------------------------------------------------- /widgets/trakt.js: -------------------------------------------------------------------------------- 1 | // trakt组件 2 | WidgetMetadata = { 3 | id: "Trakt", 4 | title: "Trakt我看&Trakt个性化推荐", 5 | modules: [ 6 | { 7 | title: "trakt我看", 8 | requiresWebView: false, 9 | functionName: "loadInterestItems", 10 | params: [ 11 | { 12 | name: "user_name", 13 | title: "用户名", 14 | type: "input", 15 | description: "需在Trakt设置里打开隐私开关,未填写情况下接口不可用", 16 | }, 17 | { 18 | name: "status", 19 | title: "状态", 20 | type: "enumeration", 21 | enumOptions: [ 22 | { 23 | title: "想看", 24 | value: "watchlist", 25 | }, 26 | { 27 | title: "在看", 28 | value: "progress", 29 | }, 30 | { 31 | title: "看过-电影", 32 | value: "history/movies/added/asc", 33 | }, 34 | { 35 | title: "看过-电视", 36 | value: "history/shows/added/asc", 37 | }, 38 | ], 39 | }, 40 | { 41 | name: "page", 42 | title: "页码", 43 | type: "page" 44 | }, 45 | ], 46 | }, 47 | { 48 | title: "Trakt个性化推荐", 49 | requiresWebView: false, 50 | functionName: "loadSuggestionItems", 51 | params: [ 52 | { 53 | name: "cookie", 54 | title: "用户Cookie", 55 | type: "input", 56 | description: "_traktsession=xxxx,未填写情况下接口不可用;可登陆网页后,通过loon,Qx等软件抓包获取Cookie", 57 | }, 58 | { 59 | name: "type", 60 | title: "类型", 61 | type: "enumeration", 62 | enumOptions: [ 63 | { 64 | title: "电影", 65 | value: "movies", 66 | }, 67 | { 68 | title: "电视", 69 | value: "shows", 70 | }, 71 | ], 72 | }, 73 | { 74 | name: "page", 75 | title: "页码", 76 | type: "page" 77 | }, 78 | ], 79 | }, 80 | { 81 | title: "Trakt片单", 82 | requiresWebView: false, 83 | functionName: "loadListItems", 84 | params: [ 85 | { 86 | name: "user_name", 87 | title: "用户名", 88 | type: "input", 89 | description: "如:giladg,未填写情况下接口不可用", 90 | }, 91 | { 92 | name: "list_name", 93 | title: "片单列表名", 94 | type: "input", 95 | description: "如:latest-4k-releases,未填写情况下接口不可用", 96 | }, 97 | { 98 | name: "sort_by", 99 | title: "排序依据", 100 | type: "enumeration", 101 | enumOptions: [ 102 | { 103 | title: "排名算法", 104 | value: "rank", 105 | }, 106 | { 107 | title: "添加时间", 108 | value: "added", 109 | }, 110 | { 111 | title: "标题", 112 | value: "title", 113 | }, 114 | { 115 | title: "发布日期", 116 | value: "released", 117 | }, 118 | { 119 | title: "内容时长", 120 | value: "runtime", 121 | }, 122 | { 123 | title: "流行度", 124 | value: "popularity", 125 | }, 126 | { 127 | title: "随机", 128 | value: "random", 129 | }, 130 | ], 131 | }, 132 | { 133 | name: "sort_how", 134 | title: "排序方向", 135 | type: "enumeration", 136 | enumOptions: [ 137 | { 138 | title: "正序", 139 | value: "asc", 140 | }, 141 | { 142 | title: "反序", 143 | value: "desc", 144 | }, 145 | ], 146 | }, 147 | { 148 | name: "page", 149 | title: "页码", 150 | type: "page" 151 | }, 152 | ], 153 | }, 154 | { 155 | title: "Trakt追剧日历", 156 | requiresWebView: false, 157 | functionName: "loadCalendarItems", 158 | params: [ 159 | { 160 | name: "cookie", 161 | title: "用户Cookie", 162 | type: "input", 163 | description: "_traktsession=xxxx,未填写情况下接口不可用;可登陆网页后,通过loon,Qx等软件抓包获取Cookie", 164 | }, 165 | { 166 | name: "start_date", 167 | title: "开始日期:n天前(0表示今天,-1表示昨天,1表示明天)", 168 | type: "input", 169 | description: "0表示今天,-1表示昨天,1表示明天,未填写情况下接口不可用", 170 | }, 171 | { 172 | name: "days", 173 | title: "天数", 174 | type: "input", 175 | description: "如:7,会返回从开始日期起的7天内的节目,未填写情况下接口不可用", 176 | }, 177 | { 178 | name: "order", 179 | title: "排序方式", 180 | type: "enumeration", 181 | enumOptions: [ 182 | { 183 | title: "日期升序", 184 | value: "asc", 185 | }, 186 | { 187 | title: "日期降序", 188 | value: "desc", 189 | }, 190 | ], 191 | }, 192 | ], 193 | }, 194 | ], 195 | version: "1.0.9", 196 | requiredVersion: "0.0.1", 197 | description: "解析Trakt想看、在看、已看、片单、追剧日历以及根据个人数据生成的个性化推荐【五折码:CHEAP.5;七折码:CHEAP】", 198 | author: "huangxd", 199 | site: "https://github.com/huangxd-/ForwardWidgets" 200 | }; 201 | 202 | function extractTraktUrlsFromResponse(responseData, minNum, maxNum) { 203 | let docId = Widget.dom.parse(responseData); 204 | let metaElements = Widget.dom.select(docId, 'meta[content^="https://trakt.tv/"]'); 205 | if (!metaElements || metaElements.length === 0) { 206 | throw new Error("未找到任何 meta content 链接"); 207 | } 208 | 209 | let traktUrls = Array.from(new Set(metaElements 210 | .map(el => el.getAttribute?.('content') || Widget.dom.attr(el, 'content')) 211 | .filter(Boolean))) 212 | .slice(minNum - 1, maxNum); 213 | return traktUrls; 214 | } 215 | 216 | function extractTraktUrlsInProgress(responseData, minNum, maxNum) { 217 | let docId = Widget.dom.parse(responseData); 218 | let mainInfoElements = Widget.dom.select(docId, 'div.col-md-15.col-sm-8.main-info'); 219 | 220 | if (!mainInfoElements || mainInfoElements.length === 0) { 221 | throw new Error("未找到任何 main-info 元素"); 222 | } 223 | 224 | let traktUrls = []; 225 | mainInfoElements.slice(minNum - 1, maxNum).forEach(element => { 226 | // 提取 href 值 227 | let linkElement = Widget.dom.select(element, 'a[href^="/shows/"]')[0]; 228 | if (!linkElement) return; 229 | 230 | let href = linkElement.getAttribute?.('href') || Widget.dom.attr(linkElement, 'href'); 231 | if (!href) return; 232 | 233 | // 提取 progress 值 234 | let progressElement = Widget.dom.select(element, 'div.progress.ticks')[0]; 235 | let progressValue = progressElement 236 | ? parseInt(progressElement.getAttribute?.('aria-valuenow') || Widget.dom.attr(progressElement, 'aria-valuenow') || '0') 237 | : 0; 238 | 239 | // 如果 progress 不是 100,添加 URL 240 | if (progressValue !== 100) { 241 | let fullUrl = `https://trakt.tv${href}`; 242 | traktUrls.push(fullUrl); 243 | } 244 | }); 245 | 246 | return Array.from(new Set(traktUrls)); 247 | } 248 | 249 | async function fetchImdbIdsFromTraktUrls(traktUrls) { 250 | let imdbIdPromises = traktUrls.map(async (url) => { 251 | try { 252 | let detailResponse = await Widget.http.get(url, { 253 | headers: { 254 | "User-Agent": 255 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 256 | "Cache-Control": "no-cache, no-store, must-revalidate", 257 | "Pragma": "no-cache", 258 | "Expires": "0", 259 | }, 260 | }); 261 | 262 | let detailDoc = Widget.dom.parse(detailResponse.data); 263 | let imdbLinkEl = Widget.dom.select(detailDoc, 'a#external-link-imdb')[0]; 264 | 265 | if (!imdbLinkEl) return null; 266 | 267 | let href = Widget.dom.attr(imdbLinkEl, 'href'); 268 | let match = href.match(/title\/(tt\d+)/); 269 | 270 | return match ? `${match[1]}` : null; 271 | } catch { 272 | return null; // 忽略单个失败请求 273 | } 274 | }); 275 | 276 | let imdbIds = [...new Set( 277 | (await Promise.all(imdbIdPromises)) 278 | .filter(Boolean) 279 | .map((item) => item) 280 | )].map((id) => ({ 281 | id, 282 | type: "imdb", 283 | })); 284 | console.log("请求imdbIds:", imdbIds) 285 | return imdbIds; 286 | } 287 | 288 | async function fetchTraktData(url, headers = {}, status, minNum, maxNum, order = "") { 289 | try { 290 | const response = await Widget.http.get(url, { 291 | headers: { 292 | "User-Agent": 293 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 294 | "Cache-Control": "no-cache, no-store, must-revalidate", 295 | "Pragma": "no-cache", 296 | "Expires": "0", 297 | ...headers, // 允许附加额外的头 298 | }, 299 | }); 300 | 301 | console.log("请求结果:", response.data); 302 | 303 | let traktUrls = []; 304 | if (status === "progress") { 305 | traktUrls = extractTraktUrlsInProgress(response.data, minNum, maxNum); 306 | } else { 307 | traktUrls = extractTraktUrlsFromResponse(response.data, minNum, maxNum); 308 | } 309 | 310 | if (order === "desc") { 311 | traktUrls = traktUrls.reverse(); 312 | } 313 | 314 | return await fetchImdbIdsFromTraktUrls(traktUrls); 315 | } catch (error) { 316 | console.error("处理失败:", error); 317 | throw error; 318 | } 319 | } 320 | 321 | async function loadInterestItems(params = {}) { 322 | try { 323 | const page = params.page; 324 | const userName = params.user_name || ""; 325 | const status = params.status || ""; 326 | const count = 20 327 | const size = status === "watchlist" ? 6 : 3 328 | const minNum = ((page - 1) % size) * count + 1 329 | const maxNum = ((page - 1) % size) * count + 20 330 | const traktPage = Math.floor((page - 1) / size) + 1 331 | 332 | if (!userName) { 333 | throw new Error("必须提供 Trakt 用户名"); 334 | } 335 | 336 | let url = `https://trakt.tv/users/${userName}/${status}?page=${traktPage}`; 337 | return await fetchTraktData(url, {}, status, minNum, maxNum); 338 | } catch (error) { 339 | console.error("处理失败:", error); 340 | throw error; 341 | } 342 | } 343 | 344 | async function loadSuggestionItems(params = {}) { 345 | try { 346 | const page = params.page; 347 | const cookie = params.cookie || ""; 348 | const type = params.type || ""; 349 | const count = 20; 350 | const minNum = (page - 1) * count + 1 351 | const maxNum = (page) * count 352 | 353 | if (!cookie) { 354 | throw new Error("必须提供用户Cookie"); 355 | } 356 | 357 | let url = `https://trakt.tv/${type}/recommendations`; 358 | return await fetchTraktData(url, {Cookie: cookie}, "", minNum, maxNum); 359 | } catch (error) { 360 | console.error("处理失败:", error); 361 | throw error; 362 | } 363 | } 364 | 365 | async function loadListItems(params = {}) { 366 | try { 367 | const page = params.page; 368 | const userName = params.user_name || ""; 369 | const listName = params.list_name || ""; 370 | const sortBy = params.sort_by || ""; 371 | const sortHow = params.sort_how || ""; 372 | const count = 20 373 | const minNum = ((page - 1) % 6) * count + 1 374 | const maxNum = ((page - 1) % 6) * count + 20 375 | const traktPage = Math.floor((page - 1) / 6) + 1 376 | 377 | if (!userName || !listName) { 378 | throw new Error("必须提供 Trakt 用户名 和 片单列表名"); 379 | } 380 | 381 | let url = `https://trakt.tv/users/${userName}/lists/${listName}?page=${traktPage}&sort=${sortBy},${sortHow}`; 382 | return await fetchTraktData(url, {}, "", minNum, maxNum); 383 | } catch (error) { 384 | console.error("处理失败:", error); 385 | throw error; 386 | } 387 | } 388 | 389 | async function loadCalendarItems(params = {}) { 390 | try { 391 | const cookie = params.cookie || ""; 392 | const startDateInput = params.start_date || ""; 393 | const days = params.days || ""; 394 | const order = params.order || ""; 395 | 396 | if (!cookie || !startDateInput || !days || !order) { 397 | throw new Error("必须提供用户Cookie、开始日期、天数及排序方式"); 398 | } 399 | 400 | const startDateOffset = parseInt(startDateInput, 10); 401 | if (isNaN(startDateOffset)) { 402 | throw new Error("开始日期必须是有效的数字"); 403 | } 404 | 405 | const today = new Date(); 406 | const startDate = new Date(today); 407 | startDate.setDate(today.getDate() + startDateOffset); 408 | 409 | // Format date as YYYY-MM-DD 410 | const formattedStartDate = startDate.toISOString().split('T')[0]; 411 | 412 | let url = `https://trakt.tv/calendars/my/shows-movies/${formattedStartDate}/${days}`; 413 | return await fetchTraktData(url, {Cookie: cookie}, "", 1, 100, order); 414 | } catch (error) { 415 | console.error("处理失败:", error); 416 | throw error; 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /widgets/yatu.js: -------------------------------------------------------------------------------- 1 | // 雅图组件 2 | WidgetMetadata = { 3 | id: "yatu", 4 | title: "雅图(每日放送+点播排行榜+评分排行榜)", 5 | modules: [ 6 | { 7 | title: "每日放送", 8 | requiresWebView: false, 9 | functionName: "loadLatestItems", 10 | params: [ 11 | { 12 | name: "genre", 13 | title: "类型", 14 | type: "enumeration", 15 | enumOptions: [ 16 | { 17 | title: "动漫", 18 | value: "sin1", 19 | }, 20 | { 21 | title: "电影", 22 | value: "sin2", 23 | }, 24 | { 25 | title: "电视剧", 26 | value: "sin3", 27 | }, 28 | ], 29 | }, 30 | { 31 | name: "start_date", 32 | title: "开始日期:n天前(0表示今天,-1表示昨天)", 33 | type: "input", 34 | description: "0表示今天,-1表示昨天,未填写情况下接口不可用", 35 | placeholders: [ 36 | { 37 | title: "今天", 38 | value: "0" 39 | }, 40 | { 41 | title: "昨天", 42 | value: "-1" 43 | }, 44 | { 45 | title: "前天", 46 | value: "-2" 47 | }, 48 | { 49 | title: "大前天", 50 | value: "-3" 51 | }, 52 | ] 53 | }, 54 | { 55 | name: "days", 56 | title: "天数(从开始日期开始的后面m天的数据)", 57 | type: "input", 58 | description: "如:3,会返回从开始日期起的3天内的节目,未填写情况下接口不可用", 59 | value: "1", 60 | placeholders: [ 61 | { 62 | title: "1", 63 | value: "1" 64 | }, 65 | { 66 | title: "2", 67 | value: "2" 68 | }, 69 | { 70 | title: "3", 71 | value: "3" 72 | }, 73 | { 74 | title: "4", 75 | value: "4" 76 | }, 77 | ] 78 | }, 79 | ], 80 | }, 81 | { 82 | title: "点播排行榜", 83 | requiresWebView: false, 84 | functionName: "loadClickItems", 85 | params: [ 86 | { 87 | name: "genre", 88 | title: "类型", 89 | type: "enumeration", 90 | enumOptions: [ 91 | { 92 | title: "连载动漫", 93 | value: "dm-lz", 94 | }, 95 | { 96 | title: "剧场动漫", 97 | value: "dm-jc", 98 | }, 99 | { 100 | title: "电影", 101 | value: "dy", 102 | }, 103 | { 104 | title: "香港电影", 105 | value: "dy-xianggan", 106 | }, 107 | { 108 | title: "欧美电影", 109 | value: "dy-om", 110 | }, 111 | { 112 | title: "电视剧", 113 | value: "tv", 114 | }, 115 | { 116 | title: "美剧", 117 | value: "tv-meiju", 118 | }, 119 | { 120 | title: "综艺", 121 | value: "tv-zy", 122 | }, 123 | ], 124 | }, 125 | { 126 | name: "sort_by", 127 | title: "时间", 128 | type: "enumeration", 129 | enumOptions: [ 130 | { 131 | title: "今日", 132 | value: "db_lz1", 133 | }, 134 | { 135 | title: "本月", 136 | value: "db_lz2", 137 | }, 138 | { 139 | title: "历史", 140 | value: "db_lz3", 141 | }, 142 | ], 143 | }, 144 | ], 145 | }, 146 | { 147 | title: "评分排行榜", 148 | requiresWebView: false, 149 | functionName: "loadScoreItems", 150 | params: [ 151 | { 152 | name: "genre", 153 | title: "类型", 154 | type: "enumeration", 155 | enumOptions: [ 156 | { 157 | title: "动漫", 158 | value: "p-dm", 159 | }, 160 | { 161 | title: "电影", 162 | value: "p-dy", 163 | }, 164 | { 165 | title: "电视剧", 166 | value: "p-tv", 167 | }, 168 | ], 169 | }, 170 | { 171 | name: "sort_by", 172 | title: "等级", 173 | type: "enumeration", 174 | enumOptions: [ 175 | { 176 | title: "非常好看", 177 | value: "tv1", 178 | }, 179 | { 180 | title: "好看", 181 | value: "tv2", 182 | }, 183 | { 184 | title: "一般", 185 | value: "tv3", 186 | }, 187 | { 188 | title: "烂片", 189 | value: "tv4", 190 | }, 191 | ], 192 | }, 193 | ], 194 | }, 195 | ], 196 | version: "1.0.4", 197 | requiredVersion: "0.0.1", 198 | description: "解析雅图每日放送更新以及各类排行榜【五折码:CHEAP.5;七折码:CHEAP】", 199 | author: "huangxd", 200 | site: "https://github.com/huangxd-/ForwardWidgets" 201 | }; 202 | 203 | // 基础获取TMDB数据方法 204 | async function fetchTmdbData(key, mediaType) { 205 | const tmdbResults = await Widget.tmdb.get(`/search/${mediaType}`, { 206 | params: { 207 | query: key, 208 | language: "zh_CN", 209 | } 210 | }); 211 | //打印结果 212 | // console.log("搜索内容:" + key) 213 | // console.log("tmdbResults:" + JSON.stringify(tmdbResults, null, 2)); 214 | // console.log("tmdbResults.total_results:" + tmdbResults.total_results); 215 | // console.log("tmdbResults.results[0]:" + tmdbResults.results[0]); 216 | return tmdbResults.results; 217 | } 218 | 219 | function getItemInfos(data, startDateInput, days, genre) { 220 | let docId = Widget.dom.parse(data); 221 | 222 | let tables = Widget.dom.select(docId, `table#${genre}`); 223 | 224 | if (!tables || tables.length === 0) { 225 | console.error(`没有解析到相应table`); 226 | return null; 227 | } 228 | 229 | let tdElements = Widget.dom.select(tables[0], 'td'); 230 | 231 | let today = new Date(); 232 | let yesterday = new Date(today); 233 | yesterday.setDate(today.getDate() - 1); 234 | let dayBeforeYesterday = new Date(today); 235 | dayBeforeYesterday.setDate(today.getDate() - 2); 236 | 237 | function formatDate(date) { 238 | let year = date.getFullYear().toString().slice(2); // Get last two digits 239 | let month = date.getMonth() + 1; // Months are 0-based 240 | let day = date.getDate(); 241 | return `${year}/${month}/${day}`; 242 | } 243 | 244 | let results = []; 245 | 246 | tdElements.forEach(td => { 247 | // Get all text content within the td 248 | let tdContent = Widget.dom.text(td).trim(); 249 | 250 | // Find the span with style="color:#666666;" for time information 251 | let timeSpan = Widget.dom.select(td, 'span[style="color:#666666;"]')[0]; 252 | let timeText = timeSpan ? Widget.dom.text(timeSpan).trim() : ''; 253 | 254 | // Process timeText 255 | let processedTime = timeText; 256 | if (/^\d{1,2}:\d{2}:\d{2}$/.test(timeText)) { 257 | // If time is in hh:mm:ss format, use today's date 258 | processedTime = formatDate(today); 259 | } else if (timeText === '昨天') { 260 | // If time is "昨天", use yesterday's date 261 | processedTime = formatDate(yesterday); 262 | } else if (timeText === '前天') { 263 | // If time is "前天", use day before yesterday's date 264 | processedTime = formatDate(dayBeforeYesterday); 265 | } 266 | 267 | // Extract the link and title from the tag 268 | let linkEl = Widget.dom.select(td, 'a')[0]; 269 | let linkHref = linkEl ? Widget.dom.attr(linkEl, 'href') : ''; 270 | let linkText = linkEl ? Widget.dom.text(linkEl).trim() : ''; 271 | 272 | // Extract the episode information from the span (if exists) 273 | let episodeSpan = Widget.dom.select(td, 'span:not([style])')[0]; 274 | let episodeText = episodeSpan ? Widget.dom.text(episodeSpan).trim() : ''; 275 | 276 | results.push({ 277 | title: linkText.replace(/ *第[^季]*季(?:~[^季]+季)?| *\d+~\d+季| *\d+季/, ''), 278 | link: linkHref, 279 | episodes: episodeText, 280 | time: processedTime, 281 | fullContent: tdContent 282 | }); 283 | }); 284 | 285 | console.log("results: ", results) 286 | 287 | today.setHours(0, 0, 0, 0); // 规范化时间 288 | 289 | // 计算开始和结束日期 290 | let startDate = new Date(today); 291 | startDate.setDate(today.getDate() + Number(startDateInput)); 292 | startDate.setHours(0, 0, 0, 0); 293 | 294 | let endDate = new Date(startDate); 295 | endDate.setDate(startDate.getDate() + Number(days) - 1); 296 | endDate.setHours(0, 0, 0, 0); 297 | 298 | console.log("startDate: ", startDate); 299 | console.log("endDate: ", endDate); 300 | 301 | // 过滤结果 302 | return results.filter(item => { 303 | // 验证日期格式 304 | if (!item.time || !/^\d{1,2}\/\d{1,2}\/\d{2}$/.test(item.time)) { 305 | return false; 306 | } 307 | 308 | // 解析日期,假设格式为 YY/MM/DD 309 | let [year, month, day] = item.time.split('/').map(Number); 310 | let currentYear = new Date().getFullYear(); 311 | let century = Math.floor(currentYear / 100) * 100; 312 | // 如果年份小于等于当前年份的两位数,假设是 2000 年代 313 | let fullYear = year <= (currentYear % 100) ? century + year : century - 100 + year; 314 | let itemDate = new Date(fullYear, month - 1, day); 315 | 316 | // 检查日期有效性 317 | if (isNaN(itemDate)) return false; 318 | 319 | itemDate.setHours(0, 0, 0, 0); 320 | return itemDate >= startDate && itemDate <= endDate; 321 | }); 322 | } 323 | 324 | async function loadLatestItems(params = {}) { 325 | try { 326 | const genre = params.genre || ""; 327 | const startDateInput = params.start_date || ""; 328 | const days = params.days || ""; 329 | 330 | if (!genre || !startDateInput || !days) { 331 | throw new Error("必须提供分类、开始日期、天数"); 332 | } 333 | 334 | const mediaTypeDict = { 335 | sin1: 'tv', 336 | sin2: 'movie', 337 | sin3: 'tv', 338 | }; 339 | 340 | const response = await Widget.http.get("http://www.yatu.tv:2082/zuijin.asp", { 341 | headers: { 342 | "User-Agent": 343 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 344 | }, 345 | }); 346 | 347 | console.log("请求结果:", response.data); 348 | 349 | const itemInfos = getItemInfos(response.data, startDateInput, days, genre); 350 | 351 | console.log("itemInfos:", itemInfos) 352 | 353 | const promises = itemInfos.map(async (itemInfo) => { 354 | // 模拟API请求 355 | const tmdbDatas = await fetchTmdbData(itemInfo.title, mediaTypeDict[genre]) 356 | 357 | if (tmdbDatas.length !== 0) { 358 | return { 359 | id: tmdbDatas[0].id, 360 | type: "tmdb", 361 | title: tmdbDatas[0].title ?? tmdbDatas[0].name, 362 | description: tmdbDatas[0].overview, 363 | releaseDate: tmdbDatas[0].release_date ?? tmdbDatas[0].first_air_date, 364 | backdropPath: tmdbDatas[0].backdrop_path, 365 | posterPath: tmdbDatas[0].poster_path, 366 | rating: tmdbDatas[0].vote_average, 367 | mediaType: mediaTypeDict[genre], 368 | }; 369 | } else { 370 | return null; 371 | } 372 | }); 373 | 374 | // 等待所有请求完成 375 | const items = (await Promise.all(promises)).filter(Boolean); 376 | 377 | console.log(items) 378 | 379 | return items; 380 | } catch (error) { 381 | console.error("处理失败:", error); 382 | throw error; 383 | } 384 | } 385 | 386 | function getClickItemInfos(data, typ) { 387 | let docId = Widget.dom.parse(data); 388 | 389 | let tables = Widget.dom.select(docId, `table#${typ}`); 390 | 391 | if (!tables || tables.length === 0) { 392 | console.error(`没有解析到相应table`); 393 | return null; 394 | } 395 | 396 | return [...new Set( 397 | Array.from( 398 | Widget.dom.select(tables[0], 'a[target="_blank"]') 399 | ).map(a => Widget.dom.text(a).trim().replace(/ *第[^季]*季(?:~[^季]+季)?| *\d+~\d+季| *\d+季/, '')) 400 | )]; 401 | } 402 | 403 | async function fetchFinalItems(genre, typ, mediaTypeDict, suffixDict) { 404 | const response = await Widget.http.get(`http://www.yatu.tv:2082/top/${genre}.${suffixDict[genre]}`, { 405 | headers: { 406 | "User-Agent": 407 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 408 | }, 409 | }); 410 | 411 | console.log("请求结果:", response.data); 412 | 413 | const itemInfos = getClickItemInfos(response.data, typ); 414 | 415 | console.log("itemInfos:", itemInfos) 416 | 417 | const promises = itemInfos.map(async (title) => { 418 | // 模拟API请求 419 | const tmdbDatas = await fetchTmdbData(title, mediaTypeDict[genre]) 420 | 421 | if (tmdbDatas.length !== 0) { 422 | return { 423 | id: tmdbDatas[0].id, 424 | type: "tmdb", 425 | title: tmdbDatas[0].title ?? tmdbDatas[0].name, 426 | description: tmdbDatas[0].overview, 427 | releaseDate: tmdbDatas[0].release_date ?? tmdbDatas[0].first_air_date, 428 | backdropPath: tmdbDatas[0].backdrop_path, 429 | posterPath: tmdbDatas[0].poster_path, 430 | rating: tmdbDatas[0].vote_average, 431 | mediaType: mediaTypeDict[genre], 432 | }; 433 | } else { 434 | return null; 435 | } 436 | }); 437 | 438 | // 等待所有请求完成 439 | const items = (await Promise.all(promises)).filter(Boolean); 440 | 441 | console.log(items) 442 | return items; 443 | } 444 | 445 | async function loadClickItems(params = {}) { 446 | try { 447 | const genre = params.genre || ""; 448 | const typ = params.sort_by || ""; 449 | 450 | if (!genre || !typ) { 451 | throw new Error("必须提供分类、时间"); 452 | } 453 | 454 | const mediaTypeDict = { 455 | 'dm-lz': 'tv', 456 | 'dm-jc': 'movie', 457 | 'dy': 'movie', 458 | 'dy-xianggan': 'movie', 459 | 'dy-om': 'movie', 460 | 'tv': 'tv', 461 | 'tv-meiju': 'tv', 462 | 'tv-zy': 'tv', 463 | }; 464 | 465 | const suffixDict = { 466 | 'dm-lz': 'htm', 467 | 'dm-jc': 'htm', 468 | 'dy': 'htm', 469 | 'dy-xianggan': 'html', 470 | 'dy-om': 'htm', 471 | 'tv': 'htm', 472 | 'tv-meiju': 'html', 473 | 'tv-zy': 'htm', 474 | }; 475 | 476 | return await fetchFinalItems(genre, typ, mediaTypeDict, suffixDict); 477 | } catch (error) { 478 | console.error("处理失败:", error); 479 | throw error; 480 | } 481 | } 482 | 483 | async function loadScoreItems(params = {}) { 484 | try { 485 | const genre = params.genre || ""; 486 | const typ = params.sort_by || ""; 487 | 488 | if (!genre || !typ) { 489 | throw new Error("必须提供分类、等级"); 490 | } 491 | 492 | const mediaTypeDict = { 493 | 'p-dm': 'tv', 494 | 'p-dy': 'movie', 495 | 'p-tv': 'tv', 496 | }; 497 | 498 | const suffixDict = { 499 | 'p-dm': 'htm', 500 | 'p-dy': 'htm', 501 | 'p-tv': 'htm', 502 | }; 503 | 504 | return await fetchFinalItems(genre, typ, mediaTypeDict, suffixDict); 505 | } catch (error) { 506 | console.error("处理失败:", error); 507 | throw error; 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /widgets/zhuijurili.js: -------------------------------------------------------------------------------- 1 | // 追剧日历组件 2 | WidgetMetadata = { 3 | id: "zhuijurili", 4 | title: "追剧日历(今/明日播出、各项榜单、今日推荐)", 5 | modules: [ 6 | { 7 | id: "todayPlay", 8 | title: "今日播出", 9 | requiresWebView: false, 10 | functionName: "loadTmdbItems", 11 | params: [ 12 | { 13 | name: "sort_by", 14 | title: "类型", 15 | type: "enumeration", 16 | value: "今天播出的剧集", 17 | enumOptions: [ 18 | { 19 | title: "剧集", 20 | value: "今天播出的剧集", 21 | }, 22 | { 23 | title: "番剧", 24 | value: "今天播出的番剧", 25 | }, 26 | { 27 | title: "国漫", 28 | value: "今天播出的国漫", 29 | }, 30 | { 31 | title: "综艺", 32 | value: "今天播出的综艺", 33 | }, 34 | ], 35 | }, 36 | ], 37 | }, 38 | { 39 | id: "tomorrowPlay", 40 | title: "明日播出", 41 | requiresWebView: false, 42 | functionName: "loadTmdbItems", 43 | params: [ 44 | { 45 | name: "sort_by", 46 | title: "类型", 47 | type: "enumeration", 48 | value: "明天播出的剧集", 49 | enumOptions: [ 50 | { 51 | title: "剧集", 52 | value: "明天播出的剧集", 53 | }, 54 | { 55 | title: "番剧", 56 | value: "明天播出的番剧", 57 | }, 58 | { 59 | title: "国漫", 60 | value: "明天播出的国漫", 61 | }, 62 | { 63 | title: "综艺", 64 | value: "明天播出的综艺", 65 | }, 66 | ], 67 | }, 68 | ], 69 | }, 70 | { 71 | id: "todayReCommand", 72 | title: "今日推荐", 73 | requiresWebView: false, 74 | functionName: "loadTmdbItems", 75 | params: [ 76 | { 77 | name: "sort_by", 78 | title: "类型", 79 | type: "constant", 80 | value: "今日推荐", 81 | }, 82 | ], 83 | }, 84 | { 85 | id: "rank", 86 | title: "各项榜单", 87 | requiresWebView: false, 88 | functionName: "loadTmdbItems", 89 | params: [ 90 | { 91 | name: "sort_by", 92 | title: "类型", 93 | type: "enumeration", 94 | value: "现正热播", 95 | enumOptions: [ 96 | { 97 | title: "现正热播", 98 | value: "现正热播", 99 | }, 100 | { 101 | title: "人气 Top 10", 102 | value: "人气 Top 10", 103 | }, 104 | { 105 | title: "新剧雷达", 106 | value: "新剧雷达", 107 | }, 108 | { 109 | title: "热门国漫", 110 | value: "热门国漫", 111 | }, 112 | { 113 | title: "已收官好剧", 114 | value: "已收官好剧", 115 | }, 116 | { 117 | title: "华语热门", 118 | value: "华语热门", 119 | }, 120 | { 121 | title: "本季新番", 122 | value: "本季新番", 123 | }, 124 | ], 125 | }, 126 | ], 127 | }, 128 | { 129 | id: "area", 130 | title: "地区榜单", 131 | requiresWebView: false, 132 | functionName: "loadTmdbItems", 133 | params: [ 134 | { 135 | name: "sort_by", 136 | title: "地区", 137 | type: "enumeration", 138 | value: "国产剧", 139 | enumOptions: [ 140 | { 141 | title: "国产剧", 142 | value: "国产剧", 143 | }, 144 | { 145 | title: "日剧", 146 | value: "日剧", 147 | }, 148 | { 149 | title: "英美剧", 150 | value: "英美剧", 151 | }, 152 | { 153 | title: "番剧", 154 | value: "番剧", 155 | }, 156 | { 157 | title: "韩剧", 158 | value: "韩剧", 159 | }, 160 | { 161 | title: "港台剧", 162 | value: "港台剧", 163 | }, 164 | ], 165 | }, 166 | ], 167 | }, 168 | ], 169 | version: "1.0.2", 170 | requiredVersion: "0.0.1", 171 | description: "解析追剧日历今/明日播出剧集/番剧/国漫/综艺、各项榜单、今日推荐等【五折码:CHEAP.5;七折码:CHEAP】", 172 | author: "huangxd", 173 | site: "https://github.com/huangxd-/ForwardWidgets" 174 | }; 175 | 176 | const API_SUFFIXES = { 177 | home1: [ 178 | "今天播出的剧集", "今天播出的番剧", 179 | "明天播出的剧集", "明天播出的番剧", 180 | "现正热播", "人气 Top 10", "新剧雷达", 181 | "热门国漫", "已收官好剧" 182 | ], 183 | home0: [ 184 | "华语热门", "本季新番", "今日推荐", 185 | "国产剧", "日剧", "英美剧", "番剧", "韩剧", "港台剧" 186 | ] 187 | }; 188 | 189 | const areaTypes = ["国产剧", "日剧", "英美剧", "番剧", "韩剧", "港台剧"]; 190 | 191 | // 生成反向映射,便于快速查找 192 | const suffixMap = {}; 193 | Object.entries(API_SUFFIXES).forEach(([suffix, values]) => { 194 | values.forEach(value => suffixMap[value] = suffix); 195 | }); 196 | 197 | // 基础获取TMDB数据方法 198 | async function fetchTmdbData(id, mediaType) { 199 | const tmdbResult = await Widget.tmdb.get(`/${mediaType}/${id}`, { 200 | headers: { 201 | "User-Agent": 202 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 203 | }, 204 | }); 205 | //打印结果 206 | // console.log("搜索内容:" + key) 207 | if (!tmdbResult) { 208 | console.log("搜索内容失败:", `/${mediaType}/${id}`); 209 | return null; 210 | } 211 | console.log("tmdbResults:" + JSON.stringify(tmdbResult, null, 2)); 212 | // console.log("tmdbResults.total_results:" + tmdbResults.total_results); 213 | // console.log("tmdbResults.results[0]:" + tmdbResults.results[0]); 214 | return tmdbResult; 215 | } 216 | 217 | async function fetchImdbItems(scItems) { 218 | const promises = scItems.map(async (scItem) => { 219 | // 模拟API请求 220 | if (!scItem || (!scItem.id && !scItem.tmdb_id)) { 221 | return null; 222 | } 223 | 224 | const mediaType = scItem.hasOwnProperty('isMovie') ? (scItem.isMovie ? 'movie' : 'tv') : 'tv'; 225 | 226 | const tmdbData = await fetchTmdbData(scItem.id ?? scItem.tmdb_id, mediaType); 227 | 228 | if (tmdbData) { 229 | return { 230 | id: tmdbData.id, 231 | type: "tmdb", 232 | title: tmdbData.title ?? tmdbData.name, 233 | description: tmdbData.overview, 234 | releaseDate: tmdbData.release_date ?? tmdbData.first_air_date, 235 | backdropPath: tmdbData.backdrop_path, 236 | posterPath: tmdbData.poster_path, 237 | rating: tmdbData.vote_average, 238 | mediaType: mediaType, 239 | }; 240 | } else { 241 | return null; 242 | } 243 | }); 244 | 245 | // 等待所有请求完成 246 | const items = (await Promise.all(promises)).filter(Boolean); 247 | 248 | return items; 249 | } 250 | 251 | async function fetchDefaultData(sort_by) { 252 | const url_prefix = "https://zjrl-1318856176.cos.accelerate.myqcloud.com"; 253 | let url = `${url_prefix}/${suffixMap[sort_by]}.json`; 254 | 255 | const response = await Widget.http.get(url, { 256 | headers: { 257 | "User-Agent": 258 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 259 | }, 260 | }); 261 | 262 | console.log("请求结果:", response.data); 263 | 264 | if (response.data) { 265 | let data; 266 | let items; 267 | if (sort_by === "今日推荐") { 268 | data = response.data.find(item => item.type === "1s"); 269 | items = data.content; 270 | } else if (areaTypes.includes(sort_by)) { 271 | data = response.data.find(item => item.type === "category"); 272 | items = data.content.find(item => item.title === sort_by).data; 273 | } else { 274 | data = response.data.find(item => item.title === sort_by); 275 | items = data.content; 276 | } 277 | console.log("items: ", items); 278 | 279 | const tmdbIds = await fetchImdbItems(items); 280 | 281 | console.log("tmdbIds: ", tmdbIds); 282 | 283 | return tmdbIds; 284 | } 285 | return []; 286 | } 287 | 288 | async function fetchOtherData(typ, sort_by) { 289 | const whichDay = sort_by.includes("今天") ? "today" : "tomorrow"; 290 | const response = await Widget.http.get(`https://proxy.hxd.ip-ddns.com/https://gist.githubusercontent.com/huangxd-/5ae61c105b417218b9e5bad7073d2f36/raw/${typ}_${whichDay}.json`, { 291 | headers: { 292 | "User-Agent": 293 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 294 | }, 295 | }); 296 | 297 | console.log("请求结果:", response.data); 298 | 299 | const tmdbIds = await fetchImdbItems(response.data); 300 | 301 | console.log("tmdbIds: ", tmdbIds); 302 | 303 | return tmdbIds; 304 | } 305 | 306 | async function loadTmdbItems(params = {}) { 307 | const sort_by = params.sort_by || ""; 308 | 309 | let res; 310 | if (sort_by === "今天播出的国漫" || sort_by === "明天播出的国漫") { 311 | res = await fetchOtherData("guoman", sort_by); 312 | } else if (sort_by === "今天播出的综艺" || sort_by === "明天播出的综艺") { 313 | res = await fetchOtherData("zongyi", sort_by); 314 | } else { 315 | res = await fetchDefaultData(sort_by); 316 | } 317 | 318 | return res; 319 | } --------------------------------------------------------------------------------