├── .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 |
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 | [](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 | }
--------------------------------------------------------------------------------