flzta commited on
Commit
f3fcf64
·
verified ·
1 Parent(s): fac9923

Upload 18 files

Browse files
Files changed (18) hide show
  1. .dockerignore +2 -0
  2. .github/workflows/docker-build.yml +43 -0
  3. .github/workflows/sync.yml +39 -0
  4. Dockerfile +12 -0
  5. LICENSE +201 -0
  6. about.html +30 -0
  7. css/styles.css +600 -0
  8. index.html +262 -19
  9. js/api.js +607 -0
  10. js/app.js +933 -0
  11. js/config.js +195 -0
  12. js/ui.js +234 -0
  13. player.html +947 -0
  14. privacy.html +29 -0
  15. readme.md +144 -0
  16. robots.txt +6 -0
  17. sitemap.xml +21 -0
  18. watch.html +11 -0
.dockerignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .vscode/
2
+ readme.md
.github/workflows/docker-build.yml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 'Build LibreTV image'
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ name: 'Build LibreTV image'
12
+ runs-on: ubuntu-latest
13
+ # Add condition to only run on original repository
14
+ if: github.repository == 'bestzwei/LibreTV'
15
+ env:
16
+ DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
17
+
18
+ steps:
19
+ - name: 'Check out repository'
20
+ uses: actions/checkout@v4.2.2
21
+ with:
22
+ fetch-depth: 0
23
+
24
+ - name: 'Set up Docker QEMU'
25
+ uses: docker/setup-qemu-action@v3.5.0
26
+
27
+ - name: 'Set up Docker Buildx'
28
+ uses: docker/setup-buildx-action@v3.10.0
29
+
30
+ - name: 'Login to DockerHub'
31
+ uses: docker/login-action@v3.4.0
32
+ with:
33
+ username: ${{ secrets.DOCKER_USERNAME }}
34
+ password: ${{ secrets.DOCKER_PASSWORD }}
35
+
36
+ - name: 'Build and push LibreTV image'
37
+ uses: docker/build-push-action@v6.14.0
38
+ with:
39
+ context: .
40
+ file: Dockerfile
41
+ push: true
42
+ tags: "${{ env.DOCKER_USERNAME }}/libretv:latest"
43
+ platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386,linux/s390x
.github/workflows/sync.yml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Upstream Sync
2
+
3
+ permissions:
4
+ contents: write
5
+
6
+ on:
7
+ schedule:
8
+ - cron: "0 4 * * *" # At 04:00, every day
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ sync_latest_from_upstream:
13
+ name: Sync latest commits from upstream repo
14
+ runs-on: ubuntu-latest
15
+ if: ${{ github.event.repository.fork }}
16
+
17
+ steps:
18
+ # Step 1: run a standard checkout action
19
+ - name: Checkout target repo
20
+ uses: actions/checkout@v3
21
+
22
+ # Step 2: run the sync action
23
+ - name: Sync upstream changes
24
+ id: sync
25
+ uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
26
+ with:
27
+ upstream_sync_repo: bestZwei/LibreTV
28
+ upstream_sync_branch: main
29
+ target_sync_branch: main
30
+ target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
31
+
32
+ # Set test_mode true to run tests instead of the true action!!
33
+ test_mode: false
34
+
35
+ - name: Sync check
36
+ if: failure()
37
+ run: |
38
+ echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
39
+ exit 1
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nginx:alpine
2
+ LABEL maintainer="LibreTV Team"
3
+ LABEL description="LibreTV - 免费在线视频搜索与观看平台"
4
+
5
+ # 复制应用文件
6
+ COPY . /usr/share/nginx/html
7
+
8
+ # 暴露端口
9
+ EXPOSE 80
10
+
11
+ # 健康检查
12
+ HEALTHCHECK --interval=30s --timeout=3s CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 2025 LibreTV Team
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.
about.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>关于我们 - LibreTV</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="css/styles.css">
9
+ <!-- ...existing head code... -->
10
+ </head>
11
+ <body class="page-bg text-white">
12
+ <div class="container mx-auto px-4 py-8">
13
+ <header class="text-center mb-8">
14
+ <h1 class="text-5xl font-bold gradient-text">关于我们</h1>
15
+ </header>
16
+ <main class="text-center">
17
+ <p class="text-gray-300 mb-4">
18
+ 本项目代码托管在 GitHub 上,欢迎访问我们的仓库:
19
+ <a href="https://github.com/bestZwei/LibreTV" class="text-blue-400 hover:underline" target="_blank" rel="noopener">https://github.com/bestZwei/LibreTV</a>
20
+ </p>
21
+ <p class="text-gray-300">
22
+ LibreTV 是一个免费的在线视频搜索平台,提供视频搜索和播放服务,致力于为用户带来最佳体验。
23
+ </p>
24
+ </main>
25
+ <footer class="mt-12 text-center">
26
+ <a href="index.html" class="text-gray-400 hover:text-white transition-colors">回到首页</a>
27
+ </footer>
28
+ </div>
29
+ </body>
30
+ </html>
css/styles.css ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* 赛博影视主题配色方案 - 柔和版 */
3
+ --primary-color: #00ccff; /* 霓虹蓝主色调 */
4
+ --primary-light: #33d6ff; /* 浅霓虹蓝变体 */
5
+ --secondary-color: #0f1622; /* 深蓝黑背景色 */
6
+ --accent-color: #ff3c78; /* 霓虹粉强调色 */
7
+ --text-color: #e6f2ff; /* 柔和的蓝白色文本 */
8
+ --text-muted: #8599b2; /* 淡蓝灰色次级文本 */
9
+ --border-color: rgba(0, 204, 255, 0.15);
10
+ --page-gradient-start: #0f1622; /* 深蓝黑起始色 */
11
+ --page-gradient-end: #192231; /* 深靛蓝结束色 */
12
+ --card-gradient-start: #121b29; /* 卡片起始色 */
13
+ --card-gradient-end: #1c2939; /* 卡片结束色 */
14
+ --card-accent: rgba(0, 204, 255, 0.12); /* 霓虹蓝卡片强调色 */
15
+ --card-hover-border: rgba(0, 204, 255, 0.5); /* 悬停边框颜色 */
16
+ }
17
+
18
+ .page-bg {
19
+ background: linear-gradient(180deg, var(--page-gradient-start), var(--page-gradient-end));
20
+ min-height: 100vh;
21
+ /* 柔和赛博点状背景 */
22
+ background-image:
23
+ linear-gradient(180deg, var(--page-gradient-start), var(--page-gradient-end)),
24
+ radial-gradient(circle at 25px 25px, rgba(0, 204, 255, 0.04) 2px, transparent 3px),
25
+ radial-gradient(circle at 75px 75px, rgba(255, 60, 120, 0.02) 1px, transparent 2px),
26
+ radial-gradient(circle at 50px 50px, rgba(150, 255, 250, 0.015) 1px, transparent 2px);
27
+ background-blend-mode: normal;
28
+ background-size: cover, 100px 100px, 50px 50px, 75px 75px;
29
+ }
30
+
31
+ button, .card-hover {
32
+ transition: all 0.3s ease;
33
+ }
34
+
35
+ /* 改进卡片适应不同内容长度 */
36
+ .card-hover {
37
+ border: 1px solid var(--border-color);
38
+ background: linear-gradient(135deg, var(--card-gradient-start), var(--card-gradient-end));
39
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
40
+ position: relative;
41
+ overflow: hidden;
42
+ border-radius: 6px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ height: 100%;
46
+ }
47
+
48
+ /* 确保卡片内容区域高度一致性 */
49
+ .card-hover .flex-grow {
50
+ min-height: 90px;
51
+ display: flex;
52
+ flex-direction: column;
53
+ }
54
+
55
+ /* 针对不同长度的标题优化显示 */
56
+ .card-hover h3 {
57
+ min-height: 3rem; /* 至少能显示2行标题 */
58
+ display: -webkit-box;
59
+ -webkit-box-orient: vertical;
60
+ overflow: hidden;
61
+ -webkit-line-clamp: 2; /* 最多显示2行 */
62
+ }
63
+
64
+ .card-hover::before {
65
+ content: "";
66
+ position: absolute;
67
+ top: 0;
68
+ left: -100%;
69
+ width: 100%;
70
+ height: 100%;
71
+ background: linear-gradient(90deg, transparent, var(--card-accent), transparent);
72
+ transition: left 0.6s ease;
73
+ }
74
+
75
+ .card-hover:hover {
76
+ border-color: var(--card-hover-border);
77
+ transform: translateY(-3px);
78
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
79
+ }
80
+
81
+ .card-hover:hover::before {
82
+ left: 100%;
83
+ }
84
+
85
+ .gradient-text {
86
+ background: linear-gradient(to right, var(--primary-color), var(--accent-color));
87
+ -webkit-background-clip: text;
88
+ -webkit-text-fill-color: transparent;
89
+ }
90
+
91
+ /* 改进设置面板样式 */
92
+ .settings-panel {
93
+ scrollbar-width: thin;
94
+ scrollbar-color: #444 #222;
95
+ transform: translateX(100%);
96
+ transition: transform 0.3s ease;
97
+ background: linear-gradient(135deg, var(--page-gradient-end), var(--page-gradient-start));
98
+ border-left: 1px solid var(--primary-color);
99
+ }
100
+
101
+ .settings-panel.show {
102
+ transform: translateX(0);
103
+ }
104
+
105
+ .settings-panel::-webkit-scrollbar {
106
+ width: 6px;
107
+ }
108
+
109
+ .settings-panel::-webkit-scrollbar-track {
110
+ background: transparent;
111
+ }
112
+
113
+ .settings-panel::-webkit-scrollbar-thumb {
114
+ background-color: #444;
115
+ border-radius: 4px;
116
+ }
117
+
118
+ /* 设置面板区块样式 */
119
+ .settings-panel .shadow-inner {
120
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.3);
121
+ transition: all 0.2s ease-in-out;
122
+ }
123
+
124
+ .settings-panel .shadow-inner:hover {
125
+ box-shadow: inset 0 2px 8px rgba(0,0,0,0.4);
126
+ }
127
+
128
+ .search-button {
129
+ background: var(--primary-color);
130
+ color: var(--text-color);
131
+ }
132
+
133
+ .search-button:hover {
134
+ background: var(--primary-light);
135
+ }
136
+
137
+ ::-webkit-scrollbar {
138
+ width: 8px;
139
+ height: 8px;
140
+ }
141
+
142
+ ::-webkit-scrollbar-track {
143
+ background: #111;
144
+ border-radius: 4px;
145
+ }
146
+
147
+ ::-webkit-scrollbar-thumb {
148
+ background: #333;
149
+ border-radius: 4px;
150
+ transition: all 0.3s ease;
151
+ }
152
+
153
+ ::-webkit-scrollbar-thumb:hover {
154
+ background: #444;
155
+ }
156
+
157
+ * {
158
+ scrollbar-width: thin;
159
+ scrollbar-color: #333 #111;
160
+ }
161
+
162
+ .search-tag {
163
+ background: linear-gradient(135deg, var(--card-gradient-start), var(--card-gradient-end));
164
+ color: var(--text-color);
165
+ padding: 0.5rem 1rem;
166
+ border-radius: 0.5rem;
167
+ font-size: 0.875rem;
168
+ border: 1px solid var(--border-color);
169
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
170
+ }
171
+
172
+ .search-tag:hover {
173
+ background: linear-gradient(135deg, var(--card-gradient-end), var(--card-gradient-start));
174
+ border-color: var(--primary-color);
175
+ }
176
+
177
+ .footer {
178
+ width: 100%;
179
+ transition: all 0.3s ease;
180
+ margin-top: auto;
181
+ background: linear-gradient(to bottom, transparent, var(--page-gradient-start));
182
+ border-top: 1px solid var(--border-color);
183
+ }
184
+
185
+ .footer a:hover {
186
+ text-decoration: underline;
187
+ }
188
+
189
+ body {
190
+ display: flex;
191
+ flex-direction: column;
192
+ min-height: 100vh;
193
+ }
194
+
195
+ .container {
196
+ flex: 1;
197
+ }
198
+
199
+ @media screen and (min-height: 800px) {
200
+ body {
201
+ display: flex;
202
+ flex-direction: column;
203
+ min-height: 100vh;
204
+ }
205
+
206
+ .container {
207
+ flex: 1;
208
+ }
209
+
210
+ .footer {
211
+ margin-top: auto;
212
+ }
213
+ }
214
+
215
+ @media screen and (max-width: 640px) {
216
+ .footer {
217
+ padding-bottom: 2rem;
218
+ }
219
+ }
220
+
221
+ /* 移动端布局优化 */
222
+ @media screen and (max-width: 768px) {
223
+ .card-hover h3 {
224
+ min-height: 2.5rem;
225
+ }
226
+
227
+ .card-hover .flex-grow {
228
+ min-height: 80px;
229
+ }
230
+ }
231
+
232
+ @keyframes fadeIn {
233
+ from { opacity: 0; }
234
+ to { opacity: 1; }
235
+ }
236
+
237
+ @keyframes fadeOut {
238
+ from { opacity: 1; }
239
+ to { opacity: 0; }
240
+ }
241
+
242
+ #modal.show {
243
+ animation: fadeIn 0.3s forwards;
244
+ }
245
+
246
+ #modal.hide {
247
+ animation: fadeOut 0.3s forwards;
248
+ }
249
+
250
+ #modal > div {
251
+ background: linear-gradient(135deg, var(--card-gradient-start), var(--card-gradient-end));
252
+ border: 1px solid var(--primary-color);
253
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7), 0 0 15px rgba(0, 204, 255, 0.1);
254
+ border-radius: 8px;
255
+ }
256
+
257
+ #modalContent button {
258
+ background: rgba(0, 204, 255, 0.08);
259
+ border: 1px solid rgba(0, 204, 255, 0.2);
260
+ transition: all 0.2s ease;
261
+ }
262
+
263
+ #modalContent button:hover {
264
+ background: rgba(0, 204, 255, 0.15);
265
+ border-color: var(--primary-color);
266
+ box-shadow: 0 0 8px rgba(0, 204, 255, 0.3);
267
+ }
268
+
269
+ #yellowFilterToggle:checked + .toggle-bg {
270
+ background-color: var(--primary-color);
271
+ }
272
+
273
+ #yellowFilterToggle:checked ~ .toggle-dot {
274
+ transform: translateX(1.5rem);
275
+ }
276
+
277
+ #yellowFilterToggle:focus + .toggle-bg,
278
+ #yellowFilterToggle:hover + .toggle-bg {
279
+ box-shadow: 0 0 0 2px rgba(0, 204, 255, 0.3);
280
+ }
281
+
282
+ /* 添加广告过滤开关的CSS */
283
+ #adFilterToggle:checked + .toggle-bg {
284
+ background-color: var(--primary-color);
285
+ }
286
+
287
+ #adFilterToggle:checked ~ .toggle-dot {
288
+ transform: translateX(1.5rem);
289
+ }
290
+
291
+ #adFilterToggle:focus + .toggle-bg,
292
+ #adFilterToggle:hover + .toggle-bg {
293
+ box-shadow: 0 0 0 2px rgba(0, 204, 255, 0.3);
294
+ }
295
+
296
+ .toggle-dot {
297
+ transition: transform 0.3s ease-in-out;
298
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
299
+ }
300
+
301
+ .toggle-bg {
302
+ transition: background-color 0.3s ease-in-out;
303
+ }
304
+
305
+ #yellowFilterToggle:checked ~ .toggle-dot {
306
+ box-shadow: 0 2px 4px rgba(0, 204, 255, 0.3);
307
+ }
308
+
309
+ #adFilterToggle:checked ~ .toggle-dot {
310
+ box-shadow: 0 2px 4px rgba(0, 204, 255, 0.3);
311
+ }
312
+
313
+ /* 添加API复选框样式 */
314
+ .form-checkbox {
315
+ appearance: none;
316
+ -webkit-appearance: none;
317
+ -moz-appearance: none;
318
+ height: 14px;
319
+ width: 14px;
320
+ background-color: #222;
321
+ border: 1px solid #333;
322
+ border-radius: 3px;
323
+ cursor: pointer;
324
+ position: relative;
325
+ outline: none;
326
+ }
327
+
328
+ .form-checkbox:checked {
329
+ background-color: var(--primary-color);
330
+ border-color: var(--primary-color);
331
+ }
332
+
333
+ .form-checkbox:checked::after {
334
+ content: '';
335
+ position: absolute;
336
+ left: 4px;
337
+ top: 1px;
338
+ width: 4px;
339
+ height: 8px;
340
+ border: solid white;
341
+ border-width: 0 2px 2px 0;
342
+ transform: rotate(45deg);
343
+ }
344
+
345
+ /* API滚动区域美化 */
346
+ #apiCheckboxes {
347
+ scrollbar-width: thin;
348
+ scrollbar-color: #444 #222;
349
+ }
350
+
351
+ #apiCheckboxes::-webkit-scrollbar {
352
+ width: 6px;
353
+ }
354
+
355
+ #apiCheckboxes::-webkit-scrollbar-track {
356
+ background: #222;
357
+ border-radius: 4px;
358
+ }
359
+
360
+ #apiCheckboxes::-webkit-scrollbar-thumb {
361
+ background-color: #444;
362
+ border-radius: 4px;
363
+ }
364
+
365
+ /* 自定义API列表样式 */
366
+ #customApisList {
367
+ scrollbar-width: thin;
368
+ scrollbar-color: #444 #222;
369
+ }
370
+
371
+ #customApisList::-webkit-scrollbar {
372
+ width: 6px;
373
+ }
374
+
375
+ #customApisList::-webkit-scrollbar-track {
376
+ background: transparent;
377
+ }
378
+
379
+ #customApisList::-webkit-scrollbar-thumb {
380
+ background-color: #444;
381
+ border-radius: 4px;
382
+ }
383
+
384
+ /* 设置面板滚动样式 */
385
+ .settings-panel {
386
+ scrollbar-width: thin;
387
+ scrollbar-color: #444 #222;
388
+ }
389
+
390
+ .settings-panel::-webkit-scrollbar {
391
+ width: 6px;
392
+ }
393
+
394
+ .settings-panel::-webkit-scrollbar-track {
395
+ background: transparent;
396
+ }
397
+
398
+ .settings-panel::-webkit-scrollbar-thumb {
399
+ background-color: #444;
400
+ border-radius: 4px;
401
+ }
402
+
403
+ /* 添加自定义API表单动画 */
404
+ #addCustomApiForm {
405
+ transition: all 0.3s ease;
406
+ max-height: 0;
407
+ opacity: 0;
408
+ overflow: hidden;
409
+ }
410
+
411
+ #addCustomApiForm.hidden {
412
+ max-height: 0;
413
+ padding: 0;
414
+ opacity: 0;
415
+ }
416
+
417
+ #addCustomApiForm:not(.hidden) {
418
+ max-height: 230px;
419
+ opacity: 1;
420
+ }
421
+
422
+ /* 成人内容API标记样式 */
423
+ .api-adult + label {
424
+ color: #ff6b8b !important;
425
+ }
426
+
427
+ /* 添加警告图标和标签样式 */
428
+ .adult-warning {
429
+ display: inline-flex;
430
+ align-items: center;
431
+ margin-left: 0.25rem;
432
+ color: #ff6b8b;
433
+ }
434
+
435
+ .adult-warning svg {
436
+ width: 12px;
437
+ height: 12px;
438
+ margin-right: 4px;
439
+ }
440
+
441
+ /* 过滤器禁用样式 */
442
+ .filter-disabled {
443
+ opacity: 0.5;
444
+ pointer-events: none;
445
+ cursor: not-allowed;
446
+ }
447
+
448
+ /* API组标题样式 */
449
+ .api-group-title {
450
+ grid-column: span 2;
451
+ padding: 0.25rem 0;
452
+ margin-top: 0.5rem;
453
+ border-top: 1px solid #333;
454
+ color: #8599b2;
455
+ font-size: 0.75rem;
456
+ text-transform: uppercase;
457
+ letter-spacing: 0.05em;
458
+ }
459
+
460
+ .api-group-title.adult {
461
+ color: #ff6b8b;
462
+ }
463
+
464
+ /* 过滤器禁用样式 - 改进版本 */
465
+ .filter-disabled {
466
+ position: relative;
467
+ }
468
+
469
+ .filter-disabled::after {
470
+ content: '';
471
+ position: absolute;
472
+ top: 0;
473
+ left: 0;
474
+ width: 100%;
475
+ height: 100%;
476
+ background-color: rgba(0,0,0,0.4);
477
+ border-radius: 0.5rem;
478
+ z-index: 5;
479
+ }
480
+
481
+ .filter-disabled > * {
482
+ opacity: 0.7;
483
+ }
484
+
485
+ .filter-disabled .toggle-bg {
486
+ background-color: #333 !important;
487
+ }
488
+
489
+ .filter-disabled .toggle-dot {
490
+ transform: translateX(0) !important;
491
+ background-color: #666 !important;
492
+ }
493
+
494
+ /* 改进过滤器禁用样式 */
495
+ .filter-disabled .filter-description {
496
+ color: #ff6b8b !important;
497
+ font-style: italic;
498
+ font-weight: 500;
499
+ }
500
+
501
+ /* 修改过滤器禁用样式,确保文字清晰可见 */
502
+ .filter-disabled {
503
+ position: relative;
504
+ }
505
+
506
+ .filter-disabled::after {
507
+ content: '';
508
+ position: absolute;
509
+ top: 0;
510
+ left: 0;
511
+ width: 100%;
512
+ height: 100%;
513
+ background-color: rgba(0,0,0,0.3);
514
+ border-radius: 0.5rem;
515
+ z-index: 5;
516
+ }
517
+
518
+ .filter-disabled > * {
519
+ opacity: 1; /* 提高子元素不透明度,保证可见性 */
520
+ z-index: 6; /* 确保内容在遮罩上方 */
521
+ position: relative;
522
+ }
523
+
524
+ /* 改进过滤器禁用状态下的描述样式 */
525
+ .filter-disabled .filter-description {
526
+ color: #ff7b9d !important; /* 更亮的粉色 */
527
+ font-style: italic;
528
+ font-weight: 500;
529
+ text-shadow: 0 0 2px rgba(0,0,0,0.8); /* 添加文字阴影提高对比度 */
530
+ }
531
+
532
+ /* 开关的禁用样式 */
533
+ .filter-disabled .toggle-bg {
534
+ background-color: #444 !important;
535
+ opacity: 0.8;
536
+ }
537
+
538
+ .filter-disabled .toggle-dot {
539
+ transform: translateX(0) !important;
540
+ background-color: #777 !important;
541
+ opacity: 0.9;
542
+ }
543
+
544
+ /* 警告提示样式改进 */
545
+ .filter-tooltip {
546
+ background-color: rgba(255, 61, 87, 0.1);
547
+ border: 1px solid rgba(255, 61, 87, 0.2);
548
+ border-radius: 0.25rem;
549
+ padding: 0.5rem;
550
+ margin-top: 0.5rem;
551
+ display: flex;
552
+ align-items: center;
553
+ font-size: 0.75rem;
554
+ line-height: 1.25;
555
+ position: relative;
556
+ z-index: 10;
557
+ }
558
+
559
+ .filter-tooltip svg {
560
+ flex-shrink: 0;
561
+ width: 14px;
562
+ height: 14px;
563
+ margin-right: 0.35rem;
564
+ }
565
+
566
+ /* 编辑按钮样式 */
567
+ .custom-api-edit {
568
+ color: #3b82f6;
569
+ transition: color 0.2s ease;
570
+ }
571
+
572
+ .custom-api-edit:hover {
573
+ color: #2563eb;
574
+ }
575
+
576
+ /* 自定义API条目样式改进 */
577
+ #customApisList .api-item {
578
+ display: flex;
579
+ align-items: center;
580
+ justify-content: space-between;
581
+ padding: 0.25rem 0.5rem;
582
+ margin-bottom: 0.25rem;
583
+ background-color: #222;
584
+ border-radius: 0.25rem;
585
+ transition: background-color 0.2s ease;
586
+ }
587
+
588
+ #customApisList .api-item:hover {
589
+ background-color: #2a2a2a;
590
+ }
591
+
592
+ /* 成人内容标签样式 */
593
+ .adult-tag {
594
+ display: inline-flex;
595
+ align-items: center;
596
+ color: #ff6b8b;
597
+ font-size: 0.7rem;
598
+ font-weight: 500;
599
+ margin-right: 0.35rem;
600
+ }
index.html CHANGED
@@ -1,19 +1,262 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LibreTV - 免费在线视频搜索与观看平台</title>
7
+ <meta name="description" content="LibreTV是一个免费的在线视频搜索平台,无广告、安全,提供来自多个视频源的内容搜索与观看服务,无需注册即可使用。">
8
+ <meta name="keywords" content="在线视频,免费视频,视频搜索,电影,电视剧,LibreTV">
9
+ <meta name="author" content="LibreTV Team">
10
+ <meta name="robots" content="index, follow">
11
+
12
+ <!-- Open Graph / Facebook -->
13
+ <meta property="og:type" content="website">
14
+ <meta property="og:url" content="https://libretv.is-an.org/">
15
+ <meta property="og:title" content="LibreTV - 免费在线视频搜索与观看平台">
16
+ <meta property="og:description" content="搜索并观看来自多个视频源的内容,支持多种设备,无需注册即可使用。">
17
+ <meta property="og:image" content="https://images.icon-icons.com/38/PNG/512/retrotv_5520.png">
18
+
19
+ <!-- Twitter -->
20
+ <meta property="twitter:card" content="summary_large_image">
21
+ <meta property="twitter:url" content="https://libretv.is-an.org/">
22
+ <meta property="twitter:title" content="LibreTV - 免费在线视频搜索与观看平台">
23
+ <meta property="twitter:description" content="搜索并观看来自多个视频源的内容,支持多种设备,无需注册即可使用。">
24
+ <meta property="twitter:image" content="https://images.icon-icons.com/38/PNG/512/retrotv_5520.png">
25
+
26
+ <!-- Favicon -->
27
+ <link rel="icon" href="https://images.icon-icons.com/38/PNG/512/retrotv_5520.png">
28
+ <link rel="apple-touch-icon" href="https://images.icon-icons.com/38/PNG/512/retrotv_5520.png">
29
+
30
+ <!-- Canonical URL -->
31
+ <link rel="canonical" href="https://libretv.is-an.org/">
32
+
33
+ <script src="https://cdn.tailwindcss.com"></script>
34
+ <link rel="stylesheet" href="css/styles.css">
35
+ </head>
36
+ <body class="page-bg text-white">
37
+ <div class="fixed top-4 right-4 z-50 flex items-center space-x-4">
38
+ <button onclick="toggleSettings(event)" class="bg-[#222] hover:bg-[#333] border border-[#333] hover:border-white rounded-lg px-4 py-2 transition-colors" aria-label="打开设置">
39
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
40
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
41
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
42
+ </svg>
43
+ </button>
44
+ </div>
45
+
46
+ <!-- 设置面板 -->
47
+ <div id="settingsPanel" class="settings-panel fixed right-0 top-0 h-full w-80 bg-[#111] border-l border-[#333] p-6 z-40 overflow-y-auto" aria-label="设置面板" aria-hidden="true">
48
+ <div class="flex justify-between items-center mb-6">
49
+ <h3 class="text-xl font-bold gradient-text">设置</h3>
50
+ <button onclick="toggleSettings()" class="text-gray-400 hover:text-white" aria-label="关闭设置">&times;</button>
51
+ </div>
52
+ <div class="space-y-5">
53
+ <!-- 数据源设置区域 -->
54
+ <div class="p-3 bg-[#151515] rounded-lg shadow-inner">
55
+ <label class="block text-sm font-medium text-gray-400 mb-3 border-b border-[#333] pb-1">数据源设置</label>
56
+
57
+ <!-- 批量操作按钮 -->
58
+ <div class="flex space-x-2 mb-3">
59
+ <button onclick="selectAllAPIs(true)" class="px-2 py-1 bg-[#333] hover:bg-[#444] text-white text-xs rounded">全选</button>
60
+ <button onclick="selectAllAPIs(false)" class="px-2 py-1 bg-[#333] hover:bg-[#444] text-white text-xs rounded">全不选</button>
61
+ <button onclick="selectAllAPIs(true, true)" class="px-2 py-1 bg-[#333] hover:bg-[#444] text-white text-xs rounded">全选普通资源</button>
62
+ </div>
63
+
64
+ <!-- API选择区域 - 使用滚动区域 -->
65
+ <div class="max-h-40 overflow-y-auto bg-[#191919] p-2 rounded-lg mb-3">
66
+ <div id="apiCheckboxes" class="grid grid-cols-2 gap-2">
67
+ <!-- 这里将动态插入API复选框 -->
68
+ </div>
69
+ </div>
70
+
71
+ <!-- API信息显示 -->
72
+ <div class="text-xs text-gray-500 flex justify-between items-center">
73
+ <span>已选API数量:<span id="selectedApiCount" class="text-white">0</span></span>
74
+ <span id="siteStatus" class="ml-2"></span>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- 自定义API管理区域 -->
79
+ <div class="p-3 bg-[#151515] rounded-lg shadow-inner">
80
+ <div class="flex justify-between items-center mb-2">
81
+ <label class="block text-sm font-medium text-gray-400 border-b border-[#333] w-full pb-1">自定义API</label>
82
+ <button onclick="showAddCustomApiForm()" class="bg-[#333] hover:bg-[#444] text-white w-6 h-6 rounded-full text-center leading-none text-lg ml-1">+</button>
83
+ </div>
84
+ <div id="customApisList" class="max-h-32 overflow-y-auto mb-2">
85
+ <!-- 自定义API将显示在这里 -->
86
+ </div>
87
+
88
+ <!-- 添加自定义API表单 (默认隐藏) -->
89
+ <div id="addCustomApiForm" class="hidden mt-2 p-2 bg-[#191919] rounded-lg">
90
+ <input type="text" id="customApiName" placeholder="API名称" class="w-full bg-[#222] border border-[#333] text-white px-2 py-1 rounded mb-2">
91
+ <input type="text" id="customApiUrl" placeholder="https://abc.com" class="w-full bg-[#222] border border-[#333] text-white px-2 py-1 rounded mb-2">
92
+ <!-- 添加成人内容切换 -->
93
+ <div class="flex items-center mb-2">
94
+ <input type="checkbox" id="customApiIsAdult" class="form-checkbox h-4 w-4 text-pink-500 bg-[#222] border border-[#333]">
95
+ <label for="customApiIsAdult" class="ml-2 text-xs text-pink-400">黄色资源站</label>
96
+ </div>
97
+ <div class="flex space-x-2">
98
+ <button onclick="addCustomApi()" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs">添加</button>
99
+ <button onclick="cancelAddCustomApi()" class="bg-[#444] hover:bg-[#555] text-white px-3 py-1 rounded text-xs">取消</button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- 内容过滤设置区域 -->
105
+ <div class="p-3 bg-[#151515] rounded-lg shadow-inner">
106
+ <label class="block text-sm font-medium text-gray-400 mb-3 border-b border-[#333] pb-1">内容过滤</label>
107
+
108
+ <!-- 黄色内容过滤开关 -->
109
+ <div class="flex flex-col mb-3 pb-3 border-b border-[#222] relative">
110
+ <div class="flex items-center justify-between">
111
+ <div>
112
+ <label class="text-sm font-medium text-gray-400">黄色内容过滤</label>
113
+ <p class="text-xs text-gray-500 mt-1 filter-description">过滤"伦理片"等黄色内容</p>
114
+ </div>
115
+ <div class="relative inline-block w-12 align-middle select-none">
116
+ <input type="checkbox" id="yellowFilterToggle" class="opacity-0 absolute w-full h-full cursor-pointer z-10">
117
+ <div class="toggle-bg bg-[#333] w-12 h-6 rounded-full transition-colors duration-300 ease-in-out"></div>
118
+ <div class="toggle-dot absolute w-5 h-5 bg-white rounded-full top-0.5 left-0.5 transition-transform duration-300 ease-in-out"></div>
119
+ </div>
120
+ </div>
121
+ <!-- 警告提示将在这里动态插入 -->
122
+ </div>
123
+
124
+ <!-- 广告过滤开关 -->
125
+ <div class="flex items-center justify-between">
126
+ <div>
127
+ <label class="text-sm font-medium text-gray-400">分片广告过滤</label>
128
+ <p class="text-xs text-gray-500 mt-1">关闭可减少旧版浏览器卡顿</p>
129
+ </div>
130
+ <div class="relative inline-block w-12 align-middle select-none">
131
+ <input type="checkbox" id="adFilterToggle" class="opacity-0 absolute w-full h-full cursor-pointer z-10">
132
+ <div class="toggle-bg bg-[#333] w-12 h-6 rounded-full transition-colors duration-300 ease-in-out"></div>
133
+ <div class="toggle-dot absolute w-5 h-5 bg-white rounded-full top-0.5 left-0.5 transition-transform duration-300 ease-in-out"></div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="container mx-auto px-4 py-8 flex flex-col h-screen">
141
+ <div class="flex-1 flex flex-col">
142
+ <!-- 网站标志和口号 -->
143
+ <header class="text-center mb-2">
144
+ <div class="flex justify-center items-center mb-4">
145
+ <svg class="w-10 h-10 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
146
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
147
+ </svg>
148
+ <h1 class="text-5xl font-bold gradient-text">LibreTV</h1>
149
+ </div>
150
+ <p class="text-gray-400 mb-8">自由观影,畅享精彩</p>
151
+ </header>
152
+
153
+ <!-- 搜索区域:默认居中 -->
154
+ <div id="searchArea" class="flex-1 flex flex-col items-center justify-center">
155
+ <h2 class="text-3xl font-bold gradient-text mb-8">视频搜索</h2>
156
+ <div class="w-full max-w-2xl">
157
+ <div class="flex">
158
+ <input type="text"
159
+ id="searchInput"
160
+ class="w-full bg-[#111] border border-[#333] text-white px-6 py-4 rounded-l-lg focus:outline-none focus:border-white transition-colors"
161
+ placeholder="搜索你喜欢的视频..."
162
+ aria-label="视频搜索框">
163
+ <button onclick="search()"
164
+ class="px-8 py-4 bg-white text-black font-medium rounded-r-lg hover:bg-gray-200 transition-colors"
165
+ aria-label="搜索按钮">
166
+ 搜索
167
+ </button>
168
+ </div>
169
+
170
+ <!-- 添加最近搜索记录部分 -->
171
+ <div id="recentSearches" class="mt-4 flex flex-wrap gap-2" aria-label="最近搜索记录">
172
+ <!-- 这里会动态插入最近的搜索记录 -->
173
+ </div>
174
+ <!-- 清除历史按钮已移至搜索历史标题行 -->
175
+ </div>
176
+ </div>
177
+
178
+ <!-- 搜索结果:初始隐藏 -->
179
+ <div id="resultsArea" class="w-full hidden">
180
+ <div id="results" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ <!-- 页脚区域 -->
187
+ <footer class="footer mt-8 py-6 border-t border-[#333] bg-[#0a0a0a]">
188
+ <div class="container mx-auto px-4">
189
+ <div class="flex flex-col md:flex-row justify-between items-center">
190
+ <div class="mb-4 md:mb-0">
191
+ <div class="flex items-center justify-center md:justify-start">
192
+ <svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
193
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
194
+ </svg>
195
+ <span class="gradient-text font-bold">LibreTV</span>
196
+ </div>
197
+ <p class="text-gray-500 text-sm mt-2 text-center md:text-left">© 2025 LibreTV - 自由观影,畅享精彩</p>
198
+ </div>
199
+
200
+ <div class="text-center md:text-right">
201
+ <p class="text-gray-500 text-sm max-w-md">
202
+ 免责声明:本站仅为视频搜索工具,不存储、上传或分发任何视频内容。
203
+ 所有视频均来自第三方API接口。如有侵权,请联系相关内容提供方。
204
+ </p>
205
+ <div class="mt-2 flex justify-center md:justify-end space-x-4">
206
+ <a href="about.html" class="text-gray-400 hover:text-white text-sm transition-colors">关于我们</a>
207
+ <a href="privacy.html" class="text-gray-400 hover:text-white text-sm transition-colors">隐私政策</a>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </footer>
213
+
214
+ <!-- 详情模态框 -->
215
+ <div id="modal" class="fixed inset-0 bg-black/95 hidden flex items-center justify-center transition-opacity duration-300">
216
+ <div class="bg-[#111] p-8 rounded-lg w-11/12 max-w-4xl border border-[#333] max-h-[90vh] flex flex-col">
217
+ <div class="flex justify-between items-center mb-6 flex-none">
218
+ <h2 id="modalTitle" class="text-2xl font-bold gradient-text break-words pr-4 max-w-[80%]"></h2>
219
+ <button onclick="closeModal()" class="text-gray-400 hover:text-white text-2xl transition-colors flex-shrink-0">&times;</button>
220
+ </div>
221
+ <div id="modalContent" class="overflow-auto flex-1 min-h-0">
222
+ <div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- 错误提示框 -->
229
+ <div id="toast" class="fixed top-4 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 opacity-0 -translate-y-full">
230
+ <p id="toastMessage"></p>
231
+ </div>
232
+
233
+ <!-- 添加 loading 提示框 -->
234
+ <div id="loading" class="fixed inset-0 bg-black/80 hidden items-center justify-center z-50">
235
+ <div class="bg-[#111] p-8 rounded-lg border border-[#333] flex items-center space-x-4">
236
+ <div class="w-8 h-8 border-4 border-white border-t-transparent rounded-full animate-spin"></div>
237
+ <p class="text-white text-lg">加载中...</p>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- JSON-LD 结构化数据 -->
242
+ <script type="application/ld+json">
243
+ {
244
+ "@context": "https://schema.org",
245
+ "@type": "WebSite",
246
+ "name": "LibreTV",
247
+ "url": "https://libretv.is-an.org/",
248
+ "description": "免费在线视频搜索与观看平台",
249
+ "potentialAction": {
250
+ "@type": "SearchAction",
251
+ "target": "https://libretv.is-an.org/?s={search_term_string}",
252
+ "query-input": "required name=search_term_string"
253
+ }
254
+ }
255
+ </script>
256
+
257
+ <script src="js/config.js"></script>
258
+ <script src="js/ui.js"></script>
259
+ <script src="js/api.js"></script>
260
+ <script src="js/app.js"></script>
261
+ </body>
262
+ </html>
js/api.js ADDED
@@ -0,0 +1,607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 改进的API请求处理函数
2
+ async function handleApiRequest(url) {
3
+ const customApi = url.searchParams.get('customApi') || '';
4
+ const source = url.searchParams.get('source') || 'heimuer';
5
+
6
+ try {
7
+ if (url.pathname === '/api/search') {
8
+ const searchQuery = url.searchParams.get('wd');
9
+ if (!searchQuery) {
10
+ throw new Error('缺少搜索参数');
11
+ }
12
+
13
+ // 验证API和source的有效性
14
+ if (source === 'custom' && !customApi) {
15
+ throw new Error('使用自定义API时必须提供API地址');
16
+ }
17
+
18
+ if (!API_SITES[source] && source !== 'custom') {
19
+ throw new Error('无效的API来源');
20
+ }
21
+
22
+ const apiUrl = customApi
23
+ ? `${customApi}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`
24
+ : `${API_SITES[source].api}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
25
+
26
+ // 添加超时处理
27
+ const controller = new AbortController();
28
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
29
+
30
+ try {
31
+ const response = await fetch(PROXY_URL + encodeURIComponent(apiUrl), {
32
+ headers: API_CONFIG.search.headers,
33
+ signal: controller.signal
34
+ });
35
+
36
+ clearTimeout(timeoutId);
37
+
38
+ if (!response.ok) {
39
+ throw new Error(`API请求失败: ${response.status}`);
40
+ }
41
+
42
+ const data = await response.json();
43
+
44
+ // 检查JSON格式的有效性
45
+ if (!data || !Array.isArray(data.list)) {
46
+ throw new Error('API返回的数据格式无效');
47
+ }
48
+
49
+ // 添加源信息到每个结果
50
+ data.list.forEach(item => {
51
+ item.source_name = source === 'custom' ? '自定义源' : API_SITES[source].name;
52
+ item.source_code = source;
53
+ // 对于自定义源,添加API URL信息
54
+ if (source === 'custom') {
55
+ item.api_url = customApi;
56
+ }
57
+ });
58
+
59
+ return JSON.stringify({
60
+ code: 200,
61
+ list: data.list || [],
62
+ });
63
+ } catch (fetchError) {
64
+ clearTimeout(timeoutId);
65
+ throw fetchError;
66
+ }
67
+ }
68
+
69
+ // 详情处理
70
+ if (url.pathname === '/api/detail') {
71
+ const id = url.searchParams.get('id');
72
+ const sourceCode = url.searchParams.get('source') || 'heimuer'; // 获取源代码
73
+
74
+ if (!id) {
75
+ throw new Error('缺少视频ID参数');
76
+ }
77
+
78
+ // 验证ID格式 - 只允许数字和有限的特殊字符
79
+ if (!/^[\w-]+$/.test(id)) {
80
+ throw new Error('无效的视频ID格式');
81
+ }
82
+
83
+ // 验证API和source的有效性
84
+ if (sourceCode === 'custom' && !customApi) {
85
+ throw new Error('使用自定义API时必须提供API地址');
86
+ }
87
+
88
+ if (!API_SITES[sourceCode] && sourceCode !== 'custom') {
89
+ throw new Error('无效的API来源');
90
+ }
91
+
92
+ // 对于特殊源,使用特殊处理方式
93
+ if ((sourceCode === 'ffzy' || sourceCode === 'jisu' || sourceCode === 'huangcang') && API_SITES[sourceCode].detail) {
94
+ return await handleSpecialSourceDetail(id, sourceCode);
95
+ }
96
+
97
+ // 如果是自定义API,并且传递了detail参数,尝试特殊处理
98
+ if (sourceCode === 'custom' && url.searchParams.get('useDetail') === 'true') {
99
+ return await handleCustomApiSpecialDetail(id, customApi);
100
+ }
101
+
102
+ const detailUrl = customApi
103
+ ? `${customApi}${API_CONFIG.detail.path}${id}`
104
+ : `${API_SITES[sourceCode].api}${API_CONFIG.detail.path}${id}`;
105
+
106
+ // 添加超时处理
107
+ const controller = new AbortController();
108
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
109
+
110
+ try {
111
+ const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
112
+ headers: API_CONFIG.detail.headers,
113
+ signal: controller.signal
114
+ });
115
+
116
+ clearTimeout(timeoutId);
117
+
118
+ if (!response.ok) {
119
+ throw new Error(`详情请求失败: ${response.status}`);
120
+ }
121
+
122
+ // 解析JSON
123
+ const data = await response.json();
124
+
125
+ // 检查返回的数据是否有效
126
+ if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) {
127
+ throw new Error('获取到的详情内容无效');
128
+ }
129
+
130
+ // 获取第一个匹配的视频详情
131
+ const videoDetail = data.list[0];
132
+
133
+ // 提取播放地址
134
+ let episodes = [];
135
+
136
+ if (videoDetail.vod_play_url) {
137
+ // 分割不同播放源
138
+ const playSources = videoDetail.vod_play_url.split('$$$');
139
+
140
+ // 提取第一个播放源的集数(通常为主要源)
141
+ if (playSources.length > 0) {
142
+ const mainSource = playSources[0];
143
+ const episodeList = mainSource.split('#');
144
+
145
+ // 从每个集数中提取URL
146
+ episodes = episodeList.map(ep => {
147
+ const parts = ep.split('$');
148
+ // 返回URL部分(通常是第二部分,如果有的话)
149
+ return parts.length > 1 ? parts[1] : '';
150
+ }).filter(url => url && (url.startsWith('http://') || url.startsWith('https://')));
151
+ }
152
+ }
153
+
154
+ // 如果没有找到播放地址,尝试使用正则表达式查找m3u8链接
155
+ if (episodes.length === 0 && videoDetail.vod_content) {
156
+ const matches = videoDetail.vod_content.match(M3U8_PATTERN) || [];
157
+ episodes = matches.map(link => link.replace(/^\$/, ''));
158
+ }
159
+
160
+ return JSON.stringify({
161
+ code: 200,
162
+ episodes: episodes,
163
+ detailUrl: detailUrl,
164
+ videoInfo: {
165
+ title: videoDetail.vod_name,
166
+ cover: videoDetail.vod_pic,
167
+ desc: videoDetail.vod_content,
168
+ type: videoDetail.type_name,
169
+ year: videoDetail.vod_year,
170
+ area: videoDetail.vod_area,
171
+ director: videoDetail.vod_director,
172
+ actor: videoDetail.vod_actor,
173
+ remarks: videoDetail.vod_remarks,
174
+ // 添加源信息
175
+ source_name: sourceCode === 'custom' ? '自定义源' : API_SITES[sourceCode].name,
176
+ source_code: sourceCode
177
+ }
178
+ });
179
+ } catch (fetchError) {
180
+ clearTimeout(timeoutId);
181
+ throw fetchError;
182
+ }
183
+ }
184
+
185
+ throw new Error('未知的API路径');
186
+ } catch (error) {
187
+ console.error('API处理错误:', error);
188
+ return JSON.stringify({
189
+ code: 400,
190
+ msg: error.message || '请求处理失败',
191
+ list: [],
192
+ episodes: [],
193
+ });
194
+ }
195
+ }
196
+
197
+ // 处理自定义API的特殊详情页
198
+ async function handleCustomApiSpecialDetail(id, customApi) {
199
+ try {
200
+ // 构建详情页URL
201
+ const detailUrl = `${customApi}/index.php/vod/detail/id/${id}.html`;
202
+
203
+ // 添加超时处理
204
+ const controller = new AbortController();
205
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
206
+
207
+ // 获取详情页HTML
208
+ const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
209
+ headers: {
210
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
211
+ },
212
+ signal: controller.signal
213
+ });
214
+
215
+ clearTimeout(timeoutId);
216
+
217
+ if (!response.ok) {
218
+ throw new Error(`自定义API详情页请求失败: ${response.status}`);
219
+ }
220
+
221
+ // 获取HTML内容
222
+ const html = await response.text();
223
+
224
+ // 使用通用模式提取m3u8链接
225
+ const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
226
+ let matches = html.match(generalPattern) || [];
227
+
228
+ // 处理链接
229
+ matches = matches.map(link => {
230
+ link = link.substring(1, link.length);
231
+ const parenIndex = link.indexOf('(');
232
+ return parenIndex > 0 ? link.substring(0, parenIndex) : link;
233
+ });
234
+
235
+ // 提取基本信息
236
+ const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
237
+ const titleText = titleMatch ? titleMatch[1].trim() : '';
238
+
239
+ const descMatch = html.match(/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/);
240
+ const descText = descMatch ? descMatch[1].replace(/<[^>]+>/g, ' ').trim() : '';
241
+
242
+ return JSON.stringify({
243
+ code: 200,
244
+ episodes: matches,
245
+ detailUrl: detailUrl,
246
+ videoInfo: {
247
+ title: titleText,
248
+ desc: descText,
249
+ source_name: '自定义源',
250
+ source_code: 'custom'
251
+ }
252
+ });
253
+ } catch (error) {
254
+ console.error(`自定义API详情获取失败:`, error);
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ // 处理极速资源详情的特殊函数
260
+ async function handleJisuDetail(id, sourceCode) {
261
+ // 直接复用通用的特殊源处理函数,传入相应参数
262
+ return await handleSpecialSourceDetail(id, sourceCode);
263
+ }
264
+
265
+ // 处理非凡影视详情的特殊函数
266
+ async function handleFFZYDetail(id, sourceCode) {
267
+ // 直接复用通用的特殊源处理函数,传入相应参数
268
+ return await handleSpecialSourceDetail(id, sourceCode);
269
+ }
270
+
271
+ // 通用特殊源详情处理函数
272
+ async function handleSpecialSourceDetail(id, sourceCode) {
273
+ try {
274
+ // 构建详情页URL(使用配置中的detail URL而不是api URL)
275
+ const detailUrl = `${API_SITES[sourceCode].detail}/index.php/vod/detail/id/${id}.html`;
276
+
277
+ // 添加超时处理
278
+ const controller = new AbortController();
279
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
280
+
281
+ // 获取详情页HTML
282
+ const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
283
+ headers: {
284
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
285
+ },
286
+ signal: controller.signal
287
+ });
288
+
289
+ clearTimeout(timeoutId);
290
+
291
+ if (!response.ok) {
292
+ throw new Error(`详情页请求失败: ${response.status}`);
293
+ }
294
+
295
+ // 获取HTML内容
296
+ const html = await response.text();
297
+
298
+ // 根据不同源类型使用不同的正则表达式
299
+ let matches = [];
300
+
301
+ if (sourceCode === 'ffzy') {
302
+ // 非凡影视使用特定的正则表达式
303
+ const ffzyPattern = /\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g;
304
+ matches = html.match(ffzyPattern) || [];
305
+ }
306
+
307
+ // 如果没有找到链接或者是其他源类型,尝试一个更通用的模式
308
+ if (matches.length === 0) {
309
+ const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
310
+ matches = html.match(generalPattern) || [];
311
+ }
312
+
313
+ // 处理链接
314
+ matches = matches.map(link => {
315
+ link = link.substring(1, link.length);
316
+ const parenIndex = link.indexOf('(');
317
+ return parenIndex > 0 ? link.substring(0, parenIndex) : link;
318
+ });
319
+
320
+ // 提取可能存在的标题、简介等基本信息
321
+ const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
322
+ const titleText = titleMatch ? titleMatch[1].trim() : '';
323
+
324
+ const descMatch = html.match(/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/);
325
+ const descText = descMatch ? descMatch[1].replace(/<[^>]+>/g, ' ').trim() : '';
326
+
327
+ return JSON.stringify({
328
+ code: 200,
329
+ episodes: matches,
330
+ detailUrl: detailUrl,
331
+ videoInfo: {
332
+ title: titleText,
333
+ desc: descText,
334
+ source_name: API_SITES[sourceCode].name,
335
+ source_code: sourceCode
336
+ }
337
+ });
338
+ } catch (error) {
339
+ console.error(`${API_SITES[sourceCode].name}详情获取失败:`, error);
340
+ throw error;
341
+ }
342
+ }
343
+
344
+ // 处理聚合搜索
345
+ async function handleAggregatedSearch(searchQuery) {
346
+ // 获取可用的API源列表(排除aggregated和custom)
347
+ const availableSources = Object.keys(API_SITES).filter(key =>
348
+ key !== 'aggregated' && key !== 'custom'
349
+ );
350
+
351
+ if (availableSources.length === 0) {
352
+ throw new Error('没有可用的API源');
353
+ }
354
+
355
+ // 创建所有API源的搜索请求
356
+ const searchPromises = availableSources.map(async (source) => {
357
+ try {
358
+ const apiUrl = `${API_SITES[source].api}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
359
+
360
+ // 使用Promise.race添加超时处理
361
+ const timeoutPromise = new Promise((_, reject) =>
362
+ setTimeout(() => reject(new Error(`${source}源搜索超时`)), 8000)
363
+ );
364
+
365
+ const fetchPromise = fetch(PROXY_URL + encodeURIComponent(apiUrl), {
366
+ headers: API_CONFIG.search.headers
367
+ });
368
+
369
+ const response = await Promise.race([fetchPromise, timeoutPromise]);
370
+
371
+ if (!response.ok) {
372
+ throw new Error(`${source}源请求失败: ${response.status}`);
373
+ }
374
+
375
+ const data = await response.json();
376
+
377
+ if (!data || !Array.isArray(data.list)) {
378
+ throw new Error(`${source}源返回的数据格式无效`);
379
+ }
380
+
381
+ // 为搜索结果添加源信息
382
+ const results = data.list.map(item => ({
383
+ ...item,
384
+ source_name: API_SITES[source].name,
385
+ source_code: source
386
+ }));
387
+
388
+ return results;
389
+ } catch (error) {
390
+ console.warn(`${source}源搜索失败:`, error);
391
+ return []; // 返回空数组表示该源搜索失败
392
+ }
393
+ });
394
+
395
+ try {
396
+ // 并行执行所有搜索请求
397
+ const resultsArray = await Promise.all(searchPromises);
398
+
399
+ // 合并所有结果
400
+ let allResults = [];
401
+ resultsArray.forEach(results => {
402
+ if (Array.isArray(results) && results.length > 0) {
403
+ allResults = allResults.concat(results);
404
+ }
405
+ });
406
+
407
+ // 如果没有搜索结果,返回空结果
408
+ if (allResults.length === 0) {
409
+ return JSON.stringify({
410
+ code: 200,
411
+ list: [],
412
+ msg: '所有源均无搜索结果'
413
+ });
414
+ }
415
+
416
+ // 去重(根据vod_id和source_code组合)
417
+ const uniqueResults = [];
418
+ const seen = new Set();
419
+
420
+ allResults.forEach(item => {
421
+ const key = `${item.source_code}_${item.vod_id}`;
422
+ if (!seen.has(key)) {
423
+ seen.add(key);
424
+ uniqueResults.push(item);
425
+ }
426
+ });
427
+
428
+ // 按照视频名称和来源排序
429
+ uniqueResults.sort((a, b) => {
430
+ // 首先按照视频名称排序
431
+ const nameCompare = (a.vod_name || '').localeCompare(b.vod_name || '');
432
+ if (nameCompare !== 0) return nameCompare;
433
+
434
+ // 如果名称相同,则按照来源排序
435
+ return (a.source_name || '').localeCompare(b.source_name || '');
436
+ });
437
+
438
+ return JSON.stringify({
439
+ code: 200,
440
+ list: uniqueResults,
441
+ });
442
+ } catch (error) {
443
+ console.error('聚合搜索处理错误:', error);
444
+ return JSON.stringify({
445
+ code: 400,
446
+ msg: '聚合搜索处理失败: ' + error.message,
447
+ list: []
448
+ });
449
+ }
450
+ }
451
+
452
+ // 处理多个自定义API源的聚合搜索
453
+ async function handleMultipleCustomSearch(searchQuery, customApiUrls) {
454
+ // 解析自定义API列表
455
+ const apiUrls = customApiUrls.split(CUSTOM_API_CONFIG.separator)
456
+ .map(url => url.trim())
457
+ .filter(url => url.length > 0 && /^https?:\/\//.test(url))
458
+ .slice(0, CUSTOM_API_CONFIG.maxSources);
459
+
460
+ if (apiUrls.length === 0) {
461
+ throw new Error('没有提供有效的自定义API地址');
462
+ }
463
+
464
+ // 为每个API创建搜索请求
465
+ const searchPromises = apiUrls.map(async (apiUrl, index) => {
466
+ try {
467
+ const fullUrl = `${apiUrl}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
468
+
469
+ // 使用Promise.race添加超时处理
470
+ const timeoutPromise = new Promise((_, reject) =>
471
+ setTimeout(() => reject(new Error(`自定义API ${index+1} 搜索超时`)), 8000)
472
+ );
473
+
474
+ const fetchPromise = fetch(PROXY_URL + encodeURIComponent(fullUrl), {
475
+ headers: API_CONFIG.search.headers
476
+ });
477
+
478
+ const response = await Promise.race([fetchPromise, timeoutPromise]);
479
+
480
+ if (!response.ok) {
481
+ throw new Error(`自定义API ${index+1} 请求失败: ${response.status}`);
482
+ }
483
+
484
+ const data = await response.json();
485
+
486
+ if (!data || !Array.isArray(data.list)) {
487
+ throw new Error(`自定义API ${index+1} 返回的数据格式无效`);
488
+ }
489
+
490
+ // 为搜索结果添加源信息
491
+ const results = data.list.map(item => ({
492
+ ...item,
493
+ source_name: `${CUSTOM_API_CONFIG.namePrefix}${index+1}`,
494
+ source_code: 'custom',
495
+ api_url: apiUrl // 保存API URL以便详情获取
496
+ }));
497
+
498
+ return results;
499
+ } catch (error) {
500
+ console.warn(`自定义API ${index+1} 搜索失败:`, error);
501
+ return []; // 返回空数组表示该源搜索失败
502
+ }
503
+ });
504
+
505
+ try {
506
+ // 并行执行所有搜索请求
507
+ const resultsArray = await Promise.all(searchPromises);
508
+
509
+ // 合并所有结果
510
+ let allResults = [];
511
+ resultsArray.forEach(results => {
512
+ if (Array.isArray(results) && results.length > 0) {
513
+ allResults = allResults.concat(results);
514
+ }
515
+ });
516
+
517
+ // 如果没有搜索结果,返回空结果
518
+ if (allResults.length === 0) {
519
+ return JSON.stringify({
520
+ code: 200,
521
+ list: [],
522
+ msg: '所有自定义API源均无搜索结果'
523
+ });
524
+ }
525
+
526
+ // 去重(根据vod_id和api_url组合)
527
+ const uniqueResults = [];
528
+ const seen = new Set();
529
+
530
+ allResults.forEach(item => {
531
+ const key = `${item.api_url || ''}_${item.vod_id}`;
532
+ if (!seen.has(key)) {
533
+ seen.add(key);
534
+ uniqueResults.push(item);
535
+ }
536
+ });
537
+
538
+ return JSON.stringify({
539
+ code: 200,
540
+ list: uniqueResults,
541
+ });
542
+ } catch (error) {
543
+ console.error('自定义API聚合搜索处理错误:', error);
544
+ return JSON.stringify({
545
+ code: 400,
546
+ msg: '自定义API聚合搜索处理失败: ' + error.message,
547
+ list: []
548
+ });
549
+ }
550
+ }
551
+
552
+ // 拦截API请求
553
+ (function() {
554
+ const originalFetch = window.fetch;
555
+
556
+ window.fetch = async function(input, init) {
557
+ const requestUrl = typeof input === 'string' ? new URL(input, window.location.origin) : input.url;
558
+
559
+ if (requestUrl.pathname.startsWith('/api/')) {
560
+ try {
561
+ const data = await handleApiRequest(requestUrl);
562
+ return new Response(data, {
563
+ headers: {
564
+ 'Content-Type': 'application/json',
565
+ 'Access-Control-Allow-Origin': '*',
566
+ },
567
+ });
568
+ } catch (error) {
569
+ return new Response(JSON.stringify({
570
+ code: 500,
571
+ msg: '服务器内部错误',
572
+ }), {
573
+ status: 500,
574
+ headers: {
575
+ 'Content-Type': 'application/json',
576
+ },
577
+ });
578
+ }
579
+ }
580
+
581
+ // 非API请求使用原始fetch
582
+ return originalFetch.apply(this, arguments);
583
+ };
584
+ })();
585
+
586
+ async function testSiteAvailability(apiUrl) {
587
+ try {
588
+ // 使用更简单的测试查询
589
+ const response = await fetch('/api/search?wd=test&customApi=' + encodeURIComponent(apiUrl), {
590
+ // 添加超时
591
+ signal: AbortSignal.timeout(5000)
592
+ });
593
+
594
+ // 检查响应状态
595
+ if (!response.ok) {
596
+ return false;
597
+ }
598
+
599
+ const data = await response.json();
600
+
601
+ // 检查API响应的有效性
602
+ return data && data.code !== 400 && Array.isArray(data.list);
603
+ } catch (error) {
604
+ console.error('站点可用性测试失败:', error);
605
+ return false;
606
+ }
607
+ }
js/app.js ADDED
@@ -0,0 +1,933 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 全局变量
2
+ let selectedAPIs = JSON.parse(localStorage.getItem('selectedAPIs') || '["heimuer"]'); // 默认选中黑木耳
3
+ let customAPIs = JSON.parse(localStorage.getItem('customAPIs') || '[]'); // 存储自定义API列表
4
+
5
+ // 添加当前播放的集数索引
6
+ let currentEpisodeIndex = 0;
7
+ // 添加当前视频的所有集数
8
+ let currentEpisodes = [];
9
+ // 添加当前视频的标题
10
+ let currentVideoTitle = '';
11
+ // 全局变量用于倒序状态
12
+ let episodesReversed = false;
13
+
14
+ // 页面初始化
15
+ document.addEventListener('DOMContentLoaded', function() {
16
+ // 初始化API复选框
17
+ initAPICheckboxes();
18
+
19
+ // 初始化自定义API列表
20
+ renderCustomAPIsList();
21
+
22
+ // 初始化显示选中的API数量
23
+ updateSelectedApiCount();
24
+
25
+ // 渲染搜索历史
26
+ renderSearchHistory();
27
+
28
+ // 设置默认API选择(如果是第一次加载)
29
+ if (!localStorage.getItem('hasInitializedDefaults')) {
30
+ // 仅选择黑木耳源
31
+ selectedAPIs = ["heimuer"];
32
+ localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
33
+
34
+ // 默认选中过滤开关
35
+ localStorage.setItem('yellowFilterEnabled', 'true');
36
+ localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, 'true');
37
+
38
+ // 标记已初始化默认值
39
+ localStorage.setItem('hasInitializedDefaults', 'true');
40
+ }
41
+
42
+ // 设置黄色内容过滤开关初始状态
43
+ const yellowFilterToggle = document.getElementById('yellowFilterToggle');
44
+ if (yellowFilterToggle) {
45
+ yellowFilterToggle.checked = localStorage.getItem('yellowFilterEnabled') === 'true';
46
+ }
47
+
48
+ // 设置广告过滤开关初始状态
49
+ const adFilterToggle = document.getElementById('adFilterToggle');
50
+ if (adFilterToggle) {
51
+ adFilterToggle.checked = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; // 默认为true
52
+ }
53
+
54
+ // 设置事件监听器
55
+ setupEventListeners();
56
+
57
+ // 初始检查成人API选中状态
58
+ setTimeout(checkAdultAPIsSelected, 100);
59
+ });
60
+
61
+ // 初始化API复选框
62
+ function initAPICheckboxes() {
63
+ const container = document.getElementById('apiCheckboxes');
64
+ container.innerHTML = '';
65
+
66
+ // 添加普通API组标题
67
+ const normalTitle = document.createElement('div');
68
+ normalTitle.className = 'api-group-title';
69
+ normalTitle.textContent = '普通资源';
70
+ container.appendChild(normalTitle);
71
+
72
+ // 创建普通API源的复选框
73
+ Object.keys(API_SITES).forEach(apiKey => {
74
+ const api = API_SITES[apiKey];
75
+ if (api.adult) return; // 跳过成人内容API,稍后添加
76
+
77
+ const checked = selectedAPIs.includes(apiKey);
78
+
79
+ const checkbox = document.createElement('div');
80
+ checkbox.className = 'flex items-center';
81
+ checkbox.innerHTML = `
82
+ <input type="checkbox" id="api_${apiKey}"
83
+ class="form-checkbox h-3 w-3 text-blue-600 bg-[#222] border border-[#333]"
84
+ ${checked ? 'checked' : ''}
85
+ data-api="${apiKey}">
86
+ <label for="api_${apiKey}" class="ml-1 text-xs text-gray-400 truncate">${api.name}</label>
87
+ `;
88
+ container.appendChild(checkbox);
89
+
90
+ // 添加事件监听器
91
+ checkbox.querySelector('input').addEventListener('change', function() {
92
+ updateSelectedAPIs();
93
+ checkAdultAPIsSelected();
94
+ });
95
+ });
96
+
97
+ // 仅在隐藏设置为false时添加成人API组
98
+ if (!HIDE_BUILTIN_ADULT_APIS) {
99
+ // 添加成人API组标题
100
+ const adultTitle = document.createElement('div');
101
+ adultTitle.className = 'api-group-title adult';
102
+ adultTitle.innerHTML = `黄色资源采集站 <span class="adult-warning">
103
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
104
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
105
+ </svg>
106
+ </span>`;
107
+ container.appendChild(adultTitle);
108
+
109
+ // 创建成人API源的复选框
110
+ Object.keys(API_SITES).forEach(apiKey => {
111
+ const api = API_SITES[apiKey];
112
+ if (!api.adult) return; // 仅添加成人内容API
113
+
114
+ const checked = selectedAPIs.includes(apiKey);
115
+
116
+ const checkbox = document.createElement('div');
117
+ checkbox.className = 'flex items-center';
118
+ checkbox.innerHTML = `
119
+ <input type="checkbox" id="api_${apiKey}"
120
+ class="form-checkbox h-3 w-3 text-blue-600 bg-[#222] border border-[#333] api-adult"
121
+ ${checked ? 'checked' : ''}
122
+ data-api="${apiKey}">
123
+ <label for="api_${apiKey}" class="ml-1 text-xs text-pink-400 truncate">${api.name}</label>
124
+ `;
125
+ container.appendChild(checkbox);
126
+
127
+ // 添加事件监听器
128
+ checkbox.querySelector('input').addEventListener('change', function() {
129
+ updateSelectedAPIs();
130
+ checkAdultAPIsSelected();
131
+ });
132
+ });
133
+ }
134
+
135
+ // 初始检查成人内容状态
136
+ checkAdultAPIsSelected();
137
+ }
138
+
139
+ // 检查是否有成人API被选中
140
+ function checkAdultAPIsSelected() {
141
+ // 查找所有内置成人API复选框
142
+ const adultBuiltinCheckboxes = document.querySelectorAll('#apiCheckboxes .api-adult:checked');
143
+
144
+ // 查找所有自定义成人API复选框
145
+ const customApiCheckboxes = document.querySelectorAll('#customApisList .api-adult:checked');
146
+
147
+ const hasAdultSelected = adultBuiltinCheckboxes.length > 0 || customApiCheckboxes.length > 0;
148
+
149
+ const yellowFilterToggle = document.getElementById('yellowFilterToggle');
150
+ const yellowFilterContainer = yellowFilterToggle.closest('div').parentNode;
151
+ const filterDescription = yellowFilterContainer.querySelector('p.filter-description');
152
+
153
+ // 如果选择了成人API,禁用黄色内容过滤器
154
+ if (hasAdultSelected) {
155
+ yellowFilterToggle.checked = false;
156
+ yellowFilterToggle.disabled = true;
157
+ localStorage.setItem('yellowFilterEnabled', 'false');
158
+
159
+ // 添加禁用样式
160
+ yellowFilterContainer.classList.add('filter-disabled');
161
+
162
+ // 修改描述文字
163
+ if (filterDescription) {
164
+ filterDescription.innerHTML = '<strong class="text-pink-300">选中黄色资源站时无法启用此过滤</strong>';
165
+ }
166
+
167
+ // 移除提示信息(如果存在)
168
+ const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip');
169
+ if (existingTooltip) {
170
+ existingTooltip.remove();
171
+ }
172
+ } else {
173
+ // 启用黄色内容过滤器
174
+ yellowFilterToggle.disabled = false;
175
+ yellowFilterContainer.classList.remove('filter-disabled');
176
+
177
+ // 恢复原来的描述文字
178
+ if (filterDescription) {
179
+ filterDescription.innerHTML = '过滤"伦理片"等黄色内容';
180
+ }
181
+
182
+ // 移除提示信息
183
+ const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip');
184
+ if (existingTooltip) {
185
+ existingTooltip.remove();
186
+ }
187
+ }
188
+ }
189
+
190
+ // 渲染自定义API列表
191
+ function renderCustomAPIsList() {
192
+ const container = document.getElementById('customApisList');
193
+ if (!container) return;
194
+
195
+ if (customAPIs.length === 0) {
196
+ container.innerHTML = '<p class="text-xs text-gray-500 text-center my-2">未添加自定义API</p>';
197
+ return;
198
+ }
199
+
200
+ container.innerHTML = '';
201
+ customAPIs.forEach((api, index) => {
202
+ const apiItem = document.createElement('div');
203
+ apiItem.className = 'flex items-center justify-between p-1 mb-1 bg-[#222] rounded';
204
+
205
+ // 根据是否是成人内容设置不同的样式
206
+ const textColorClass = api.isAdult ? 'text-pink-400' : 'text-white';
207
+
208
+ // 将(18+)标记移到最前面
209
+ const adultTag = api.isAdult ? '<span class="text-xs text-pink-400 mr-1">(18+)</span>' : '';
210
+
211
+ apiItem.innerHTML = `
212
+ <div class="flex items-center flex-1 min-w-0">
213
+ <input type="checkbox" id="custom_api_${index}"
214
+ class="form-checkbox h-3 w-3 text-blue-600 mr-1 ${api.isAdult ? 'api-adult' : ''}"
215
+ ${selectedAPIs.includes('custom_' + index) ? 'checked' : ''}
216
+ data-custom-index="${index}">
217
+ <div class="flex-1 min-w-0">
218
+ <div class="text-xs font-medium ${textColorClass} truncate">
219
+ ${adultTag}${api.name}
220
+ </div>
221
+ <div class="text-xs text-gray-500 truncate">${api.url}</div>
222
+ </div>
223
+ </div>
224
+ <div class="flex items-center">
225
+ <button class="text-blue-500 hover:text-blue-700 text-xs px-1" onclick="editCustomApi(${index})">✎</button>
226
+ <button class="text-red-500 hover:text-red-700 text-xs px-1" onclick="removeCustomApi(${index})">✕</button>
227
+ </div>
228
+ `;
229
+ container.appendChild(apiItem);
230
+
231
+ // 添加事件监听器
232
+ apiItem.querySelector('input').addEventListener('change', function() {
233
+ updateSelectedAPIs();
234
+ checkAdultAPIsSelected();
235
+ });
236
+ });
237
+ }
238
+
239
+ // 编辑自定义API
240
+ function editCustomApi(index) {
241
+ if (index < 0 || index >= customAPIs.length) return;
242
+
243
+ const api = customAPIs[index];
244
+
245
+ // 填充表单数据
246
+ const nameInput = document.getElementById('customApiName');
247
+ const urlInput = document.getElementById('customApiUrl');
248
+ const isAdultInput = document.getElementById('customApiIsAdult');
249
+
250
+ nameInput.value = api.name;
251
+ urlInput.value = api.url;
252
+ if (isAdultInput) isAdultInput.checked = api.isAdult || false;
253
+
254
+ // 显示表单
255
+ const form = document.getElementById('addCustomApiForm');
256
+ if (form) {
257
+ form.classList.remove('hidden');
258
+
259
+ // 替换表单按钮操作
260
+ const buttonContainer = form.querySelector('div:last-child');
261
+ buttonContainer.innerHTML = `
262
+ <button onclick="updateCustomApi(${index})" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs">更新</button>
263
+ <button onclick="cancelEditCustomApi()" class="bg-[#444] hover:bg-[#555] text-white px-3 py-1 rounded text-xs">取消</button>
264
+ `;
265
+ }
266
+ }
267
+
268
+ // 更新自定义API
269
+ function updateCustomApi(index) {
270
+ if (index < 0 || index >= customAPIs.length) return;
271
+
272
+ const nameInput = document.getElementById('customApiName');
273
+ const urlInput = document.getElementById('customApiUrl');
274
+ const isAdultInput = document.getElementById('customApiIsAdult');
275
+
276
+ const name = nameInput.value.trim();
277
+ let url = urlInput.value.trim();
278
+ const isAdult = isAdultInput ? isAdultInput.checked : false;
279
+
280
+ if (!name || !url) {
281
+ showToast('请输入API名称和链接', 'warning');
282
+ return;
283
+ }
284
+
285
+ // 确保URL格式正确
286
+ if (!/^https?:\/\/.+/.test(url)) {
287
+ showToast('API链接格式不正确,需以http://或https://开头', 'warning');
288
+ return;
289
+ }
290
+
291
+ // 移除URL末尾的斜杠
292
+ if (url.endsWith('/')) {
293
+ url = url.slice(0, -1);
294
+ }
295
+
296
+ // 更新API信息
297
+ customAPIs[index] = { name, url, isAdult };
298
+ localStorage.setItem('customAPIs', JSON.stringify(customAPIs));
299
+
300
+ // 重新渲染自定义API列表
301
+ renderCustomAPIsList();
302
+
303
+ // 重新检查成人API选中状态
304
+ checkAdultAPIsSelected();
305
+
306
+ // 恢复添加按钮
307
+ restoreAddCustomApiButtons();
308
+
309
+ // 清空表单并隐藏
310
+ nameInput.value = '';
311
+ urlInput.value = '';
312
+ if (isAdultInput) isAdultInput.checked = false;
313
+ document.getElementById('addCustomApiForm').classList.add('hidden');
314
+
315
+ showToast('已更新自定义API: ' + name, 'success');
316
+ }
317
+
318
+ // 取消编辑自定义API
319
+ function cancelEditCustomApi() {
320
+ // 清空表单
321
+ document.getElementById('customApiName').value = '';
322
+ document.getElementById('customApiUrl').value = '';
323
+ const isAdultInput = document.getElementById('customApiIsAdult');
324
+ if (isAdultInput) isAdultInput.checked = false;
325
+
326
+ // 隐藏表单
327
+ document.getElementById('addCustomApiForm').classList.add('hidden');
328
+
329
+ // 恢复添加按钮
330
+ restoreAddCustomApiButtons();
331
+ }
332
+
333
+ // 恢复自定义API添加按钮
334
+ function restoreAddCustomApiButtons() {
335
+ const form = document.getElementById('addCustomApiForm');
336
+ const buttonContainer = form.querySelector('div:last-child');
337
+ buttonContainer.innerHTML = `
338
+ <button onclick="addCustomApi()" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs">添加</button>
339
+ <button onclick="cancelAddCustomApi()" class="bg-[#444] hover:bg-[#555] text-white px-3 py-1 rounded text-xs">取消</button>
340
+ `;
341
+ }
342
+
343
+ // 更新选中的API列表
344
+ function updateSelectedAPIs() {
345
+ // 获取所有内置API复选框
346
+ const builtInApiCheckboxes = document.querySelectorAll('#apiCheckboxes input:checked');
347
+
348
+ // 获取选中的内置API
349
+ const builtInApis = Array.from(builtInApiCheckboxes).map(input => input.dataset.api);
350
+
351
+ // 获取选中的自定义API
352
+ const customApiCheckboxes = document.querySelectorAll('#customApisList input:checked');
353
+ const customApiIndices = Array.from(customApiCheckboxes).map(input => 'custom_' + input.dataset.customIndex);
354
+
355
+ // 合并内置和自定义API
356
+ selectedAPIs = [...builtInApis, ...customApiIndices];
357
+
358
+ // 保存到localStorage
359
+ localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
360
+
361
+ // 更新显示选中的API数量
362
+ updateSelectedApiCount();
363
+ }
364
+
365
+ // 更新选中的API数量显示
366
+ function updateSelectedApiCount() {
367
+ const countEl = document.getElementById('selectedApiCount');
368
+ if (countEl) {
369
+ countEl.textContent = selectedAPIs.length;
370
+ }
371
+ }
372
+
373
+ // 全选或取消全选API
374
+ function selectAllAPIs(selectAll = true, excludeAdult = false) {
375
+ const checkboxes = document.querySelectorAll('#apiCheckboxes input[type="checkbox"]');
376
+
377
+ checkboxes.forEach(checkbox => {
378
+ if (excludeAdult && checkbox.classList.contains('api-adult')) {
379
+ checkbox.checked = false;
380
+ } else {
381
+ checkbox.checked = selectAll;
382
+ }
383
+ });
384
+
385
+ updateSelectedAPIs();
386
+ checkAdultAPIsSelected();
387
+ }
388
+
389
+ // 显示添加自定义API表单
390
+ function showAddCustomApiForm() {
391
+ const form = document.getElementById('addCustomApiForm');
392
+ if (form) {
393
+ form.classList.remove('hidden');
394
+ }
395
+ }
396
+
397
+ // 取消添加自定义API - 修改函数来重用恢复按钮逻辑
398
+ function cancelAddCustomApi() {
399
+ const form = document.getElementById('addCustomApiForm');
400
+ if (form) {
401
+ form.classList.add('hidden');
402
+ document.getElementById('customApiName').value = '';
403
+ document.getElementById('customApiUrl').value = '';
404
+ const isAdultInput = document.getElementById('customApiIsAdult');
405
+ if (isAdultInput) isAdultInput.checked = false;
406
+
407
+ // 确保按钮是添加按钮
408
+ restoreAddCustomApiButtons();
409
+ }
410
+ }
411
+
412
+ // 添加自定义API
413
+ function addCustomApi() {
414
+ const nameInput = document.getElementById('customApiName');
415
+ const urlInput = document.getElementById('customApiUrl');
416
+ const isAdultInput = document.getElementById('customApiIsAdult');
417
+
418
+ const name = nameInput.value.trim();
419
+ let url = urlInput.value.trim();
420
+ const isAdult = isAdultInput ? isAdultInput.checked : false;
421
+
422
+ if (!name || !url) {
423
+ showToast('请输入API名称和链接', 'warning');
424
+ return;
425
+ }
426
+
427
+ // 确保URL格式正确
428
+ if (!/^https?:\/\/.+/.test(url)) {
429
+ showToast('API链接格式不正确,需以http://或https://开头', 'warning');
430
+ return;
431
+ }
432
+
433
+ // 移除URL末尾的斜杠
434
+ if (url.endsWith('/')) {
435
+ url = url.slice(0, -1);
436
+ }
437
+
438
+ // 添加到自定义API列表 - 增加isAdult属性
439
+ customAPIs.push({ name, url, isAdult });
440
+ localStorage.setItem('customAPIs', JSON.stringify(customAPIs));
441
+
442
+ // 默认选中新添加的API
443
+ const newApiIndex = customAPIs.length - 1;
444
+ selectedAPIs.push('custom_' + newApiIndex);
445
+ localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
446
+
447
+ // 重新渲染自定义API列表
448
+ renderCustomAPIsList();
449
+
450
+ // 更新选中的API数量
451
+ updateSelectedApiCount();
452
+
453
+ // 重新检查成人API选中状态
454
+ checkAdultAPIsSelected();
455
+
456
+ // 清空表单并隐藏
457
+ nameInput.value = '';
458
+ urlInput.value = '';
459
+ if (isAdultInput) isAdultInput.checked = false;
460
+ document.getElementById('addCustomApiForm').classList.add('hidden');
461
+
462
+ showToast('已添加自定义API: ' + name, 'success');
463
+ }
464
+
465
+ // 移除自定义API
466
+ function removeCustomApi(index) {
467
+ if (index < 0 || index >= customAPIs.length) return;
468
+
469
+ const apiName = customAPIs[index].name;
470
+
471
+ // 从列表中移除API
472
+ customAPIs.splice(index, 1);
473
+ localStorage.setItem('customAPIs', JSON.stringify(customAPIs));
474
+
475
+ // 从选中列表中移除此API
476
+ const customApiId = 'custom_' + index;
477
+ selectedAPIs = selectedAPIs.filter(id => id !== customApiId);
478
+
479
+ // 更新大于此索引的自定义API索引
480
+ selectedAPIs = selectedAPIs.map(id => {
481
+ if (id.startsWith('custom_')) {
482
+ const currentIndex = parseInt(id.replace('custom_', ''));
483
+ if (currentIndex > index) {
484
+ return 'custom_' + (currentIndex - 1);
485
+ }
486
+ }
487
+ return id;
488
+ });
489
+
490
+ localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
491
+
492
+ // 重新渲染自定义API列表
493
+ renderCustomAPIsList();
494
+
495
+ // 更新选中的API数量
496
+ updateSelectedApiCount();
497
+
498
+ // 重新检查成人API选中状态
499
+ checkAdultAPIsSelected();
500
+
501
+ showToast('已移除自定义API: ' + apiName, 'info');
502
+ }
503
+
504
+ // 设置事件监听器
505
+ function setupEventListeners() {
506
+ // 回车搜索
507
+ document.getElementById('searchInput').addEventListener('keypress', function(e) {
508
+ if (e.key === 'Enter') {
509
+ search();
510
+ }
511
+ });
512
+
513
+ // 点击外部关闭设置面板
514
+ document.addEventListener('click', function(e) {
515
+ const panel = document.getElementById('settingsPanel');
516
+ const settingsButton = document.querySelector('button[onclick="toggleSettings(event)"]');
517
+
518
+ if (!panel.contains(e.target) && !settingsButton.contains(e.target) && panel.classList.contains('show')) {
519
+ panel.classList.remove('show');
520
+ }
521
+ });
522
+
523
+ // 黄色内容过滤开关事件绑定
524
+ const yellowFilterToggle = document.getElementById('yellowFilterToggle');
525
+ if (yellowFilterToggle) {
526
+ yellowFilterToggle.addEventListener('change', function(e) {
527
+ localStorage.setItem('yellowFilterEnabled', e.target.checked);
528
+ });
529
+ }
530
+
531
+ // 广告过滤开关事件绑定
532
+ const adFilterToggle = document.getElementById('adFilterToggle');
533
+ if (adFilterToggle) {
534
+ adFilterToggle.addEventListener('change', function(e) {
535
+ localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, e.target.checked);
536
+ });
537
+ }
538
+ }
539
+
540
+ // 重置搜索区域
541
+ function resetSearchArea() {
542
+ // 清理搜索结果
543
+ document.getElementById('results').innerHTML = '';
544
+ document.getElementById('searchInput').value = '';
545
+
546
+ // 恢复搜索区域的样式
547
+ document.getElementById('searchArea').classList.add('flex-1');
548
+ document.getElementById('searchArea').classList.remove('mb-8');
549
+ document.getElementById('resultsArea').classList.add('hidden');
550
+
551
+ // 确保页脚正确显示,移除相对定位
552
+ const footer = document.querySelector('.footer');
553
+ if (footer) {
554
+ footer.style.position = '';
555
+ }
556
+ }
557
+
558
+ // 获取自定义API信息
559
+ function getCustomApiInfo(customApiIndex) {
560
+ const index = parseInt(customApiIndex);
561
+ if (isNaN(index) || index < 0 || index >= customAPIs.length) {
562
+ return null;
563
+ }
564
+ return customAPIs[index];
565
+ }
566
+
567
+ // 搜索功能 - 修改为支持多选API
568
+ async function search() {
569
+ const query = document.getElementById('searchInput').value.trim();
570
+
571
+ if (!query) {
572
+ showToast('请输入搜索内容', 'info');
573
+ return;
574
+ }
575
+
576
+ if (selectedAPIs.length === 0) {
577
+ showToast('请至少选择一个API源', 'warning');
578
+ return;
579
+ }
580
+
581
+ showLoading();
582
+
583
+ try {
584
+ // 保存搜索历史
585
+ saveSearchHistory(query);
586
+
587
+ // 从所有选中的API源搜索
588
+ let allResults = [];
589
+ const searchPromises = selectedAPIs.map(async (apiId) => {
590
+ try {
591
+ let apiUrl, apiName;
592
+
593
+ // 处理自定义API
594
+ if (apiId.startsWith('custom_')) {
595
+ const customIndex = apiId.replace('custom_', '');
596
+ const customApi = getCustomApiInfo(customIndex);
597
+ if (!customApi) return [];
598
+
599
+ apiUrl = customApi.url + API_CONFIG.search.path + encodeURIComponent(query);
600
+ apiName = customApi.name;
601
+ } else {
602
+ // 内置API
603
+ if (!API_SITES[apiId]) return [];
604
+ apiUrl = API_SITES[apiId].api + API_CONFIG.search.path + encodeURIComponent(query);
605
+ apiName = API_SITES[apiId].name;
606
+ }
607
+
608
+ // 添加超时处理
609
+ const controller = new AbortController();
610
+ const timeoutId = setTimeout(() => controller.abort(), 8000);
611
+
612
+ const response = await fetch(PROXY_URL + encodeURIComponent(apiUrl), {
613
+ headers: API_CONFIG.search.headers,
614
+ signal: controller.signal
615
+ });
616
+
617
+ clearTimeout(timeoutId);
618
+
619
+ if (!response.ok) {
620
+ return [];
621
+ }
622
+
623
+ const data = await response.json();
624
+
625
+ if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) {
626
+ return [];
627
+ }
628
+
629
+ // 添加源信息到每个结果
630
+ const results = data.list.map(item => ({
631
+ ...item,
632
+ source_name: apiName,
633
+ source_code: apiId,
634
+ api_url: apiId.startsWith('custom_') ? getCustomApiInfo(apiId.replace('custom_', ''))?.url : undefined
635
+ }));
636
+
637
+ return results;
638
+ } catch (error) {
639
+ console.warn(`API ${apiId} 搜索失败:`, error);
640
+ return [];
641
+ }
642
+ });
643
+
644
+ // 等待所有搜索请求完成
645
+ const resultsArray = await Promise.all(searchPromises);
646
+
647
+ // 合并所有结果
648
+ resultsArray.forEach(results => {
649
+ if (Array.isArray(results) && results.length > 0) {
650
+ allResults = allResults.concat(results);
651
+ }
652
+ });
653
+
654
+ // 显示结果区域,调整搜索区域
655
+ document.getElementById('searchArea').classList.remove('flex-1');
656
+ document.getElementById('searchArea').classList.add('mb-8');
657
+ document.getElementById('resultsArea').classList.remove('hidden');
658
+
659
+ const resultsDiv = document.getElementById('results');
660
+
661
+ // 如果没有结果
662
+ if (!allResults || allResults.length === 0) {
663
+ resultsDiv.innerHTML = `
664
+ <div class="col-span-full text-center py-16">
665
+ <svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
666
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
667
+ d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
668
+ </svg>
669
+ <h3 class="mt-2 text-lg font-medium text-gray-400">没有找到匹配的结果</h3>
670
+ <p class="mt-1 text-sm text-gray-500">请尝试其他关键词或更换数据源</p>
671
+ </div>
672
+ `;
673
+ hideLoading();
674
+ return;
675
+ }
676
+
677
+ // 处理搜索结果过滤:如果启用了黄���内容过滤,则过滤掉分类含有敏感内容的项目
678
+ const yellowFilterEnabled = localStorage.getItem('yellowFilterEnabled') === 'true';
679
+ if (yellowFilterEnabled) {
680
+ const banned = ['伦理片','门事件','萝莉少女','制服诱惑','国产传媒','cosplay','黑丝诱惑','无码','日本无码','有码','日本有码','SWAG','网红主播', '色情片','同性片','福利视频','福利片'];
681
+ allResults = allResults.filter(item => {
682
+ const typeName = item.type_name || '';
683
+ return !banned.some(keyword => typeName.includes(keyword));
684
+ });
685
+ }
686
+
687
+ // 添加XSS保护,使用textContent和属性转义
688
+ resultsDiv.innerHTML = allResults.map(item => {
689
+ // ...existing code for rendering results...
690
+ const safeId = item.vod_id ? item.vod_id.toString().replace(/[^\w-]/g, '') : '';
691
+ const safeName = (item.vod_name || '').toString()
692
+ .replace(/</g, '&lt;')
693
+ .replace(/>/g, '&gt;')
694
+ .replace(/"/g, '&quot;');
695
+ const sourceInfo = item.source_name ?
696
+ `<span class="bg-[#222] text-xs px-2 py-1 rounded-full">${item.source_name}</span>` : '';
697
+ const sourceCode = item.source_code || '';
698
+
699
+ // 添加API URL属性,用于详情获取
700
+ const apiUrlAttr = item.api_url ?
701
+ `data-api-url="${item.api_url.replace(/"/g, '&quot;')}"` : '';
702
+
703
+ // 重新设计的卡片布局 - 支持更好的封面图显示
704
+ const hasCover = item.vod_pic && item.vod_pic.startsWith('http');
705
+
706
+ return `
707
+ <div class="card-hover bg-[#111] rounded-lg overflow-hidden cursor-pointer transition-all hover:scale-[1.02] h-full"
708
+ onclick="showDetails('${safeId}','${safeName}','${sourceCode}')" ${apiUrlAttr}>
709
+ <div class="md:flex">
710
+ ${hasCover ? `
711
+ <div class="md:w-1/4 relative overflow-hidden">
712
+ <div class="w-full h-40 md:h-full">
713
+ <img src="${item.vod_pic}" alt="${safeName}"
714
+ class="w-full h-full object-cover transition-transform hover:scale-110"
715
+ onerror="this.onerror=null; this.src='https://via.placeholder.com/300x450?text=无封面'; this.classList.add('object-contain');"
716
+ loading="lazy">
717
+ <div class="absolute inset-0 bg-gradient-to-t from-[#111] to-transparent opacity-60"></div>
718
+ </div>
719
+ </div>` : ''}
720
+
721
+ <div class="p-3 flex flex-col flex-grow ${hasCover ? 'md:w-3/4' : 'w-full'}">
722
+ <div class="flex-grow">
723
+ <h3 class="text-lg font-semibold mb-2 break-words">${safeName}</h3>
724
+
725
+ <div class="flex flex-wrap gap-1 mb-2">
726
+ ${(item.type_name || '').toString().replace(/</g, '&lt;') ?
727
+ `<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-blue-500 text-blue-300">
728
+ ${(item.type_name || '').toString().replace(/</g, '&lt;')}
729
+ </span>` : ''}
730
+ ${(item.vod_year || '') ?
731
+ `<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-purple-500 text-purple-300">
732
+ ${item.vod_year}
733
+ </span>` : ''}
734
+ </div>
735
+ <p class="text-gray-400 text-xs h-9 overflow-hidden">
736
+ ${(item.vod_remarks || '暂无介绍').toString().replace(/</g, '&lt;')}
737
+ </p>
738
+ </div>
739
+
740
+ <div class="flex justify-between items-center mt-2 pt-2 border-t border-gray-800">
741
+ ${sourceInfo ? `<div>${sourceInfo}</div>` : '<div></div>'}
742
+ <div>
743
+ <span class="text-xs text-gray-500 flex items-center">
744
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
745
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
746
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
747
+ </svg>
748
+ 点击播放
749
+ </span>
750
+ </div>
751
+ </div>
752
+ </div>
753
+ </div>
754
+ </div>
755
+ `;
756
+ }).join('');
757
+ } catch (error) {
758
+ console.error('搜索错误:', error);
759
+ if (error.name === 'AbortError') {
760
+ showToast('搜索请求超时,请检查网络连接', 'error');
761
+ } else {
762
+ showToast('搜索请求失败,请稍后重试', 'error');
763
+ }
764
+ } finally {
765
+ hideLoading();
766
+ }
767
+ }
768
+
769
+ // 显示详情 - 修改为支持自定义API
770
+ async function showDetails(id, vod_name, sourceCode) {
771
+ if (!id) {
772
+ showToast('视频ID无效', 'error');
773
+ return;
774
+ }
775
+
776
+ showLoading();
777
+ try {
778
+ // 构建API参数
779
+ let apiParams = '';
780
+
781
+ // 处理自定义API源
782
+ if (sourceCode.startsWith('custom_')) {
783
+ const customIndex = sourceCode.replace('custom_', '');
784
+ const customApi = getCustomApiInfo(customIndex);
785
+ if (!customApi) {
786
+ showToast('自定义API配置无效', 'error');
787
+ hideLoading();
788
+ return;
789
+ }
790
+
791
+ apiParams = '&customApi=' + encodeURIComponent(customApi.url) + '&source=custom';
792
+ } else {
793
+ // 内置API
794
+ apiParams = '&source=' + sourceCode;
795
+ }
796
+
797
+ const response = await fetch('/api/detail?id=' + encodeURIComponent(id) + apiParams);
798
+
799
+ const data = await response.json();
800
+
801
+ // ...existing code for showing details...
802
+ const modal = document.getElementById('modal');
803
+ const modalTitle = document.getElementById('modalTitle');
804
+ const modalContent = document.getElementById('modalContent');
805
+
806
+ // 显示来源信息
807
+ const sourceName = data.videoInfo && data.videoInfo.source_name ?
808
+ ` <span class="text-sm font-normal text-gray-400">(${data.videoInfo.source_name})</span>` : '';
809
+
810
+ // 不对标题进行截断处理,允许完整显示
811
+ modalTitle.innerHTML = `<span class="break-words">${vod_name || '未知视频'}</span>${sourceName}`;
812
+ currentVideoTitle = vod_name || '未知视频';
813
+
814
+ if (data.episodes && data.episodes.length > 0) {
815
+ // 安全处理集数URL
816
+ const safeEpisodes = data.episodes.map(url => {
817
+ try {
818
+ // 确保URL是有效的并且是http或https开头
819
+ return url && (url.startsWith('http://') || url.startsWith('https://'))
820
+ ? url.replace(/"/g, '&quot;')
821
+ : '';
822
+ } catch (e) {
823
+ return '';
824
+ }
825
+ }).filter(url => url); // 过滤掉空URL
826
+
827
+ // 保存当前视频的所有集数
828
+ currentEpisodes = safeEpisodes;
829
+ episodesReversed = false; // 默认正序
830
+ modalContent.innerHTML = `
831
+ <div class="flex justify-end mb-2">
832
+ <button onclick="toggleEpisodeOrder()" class="px-4 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-semibold rounded-full shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 flex items-center justify-center space-x-2">
833
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
834
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v3.586L7.707 9.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 10.586V7z" clip-rule="evenodd" />
835
+ </svg>
836
+ <span>倒序排列</span>
837
+ </button>
838
+ </div>
839
+ <div id="episodesGrid" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
840
+ ${renderEpisodes(vod_name)}
841
+ </div>
842
+ `;
843
+ } else {
844
+ modalContent.innerHTML = '<p class="text-center text-gray-400 py-8">没有找到可播放的视频</p>';
845
+ }
846
+
847
+ modal.classList.remove('hidden');
848
+ } catch (error) {
849
+ console.error('获取详情错误:', error);
850
+ showToast('获取详情失败,请稍后重试', 'error');
851
+ } finally {
852
+ hideLoading();
853
+ }
854
+ }
855
+
856
+ // 更新播放视频函数,修改为在新标签页中打开播放页面
857
+ function playVideo(url, vod_name, episodeIndex = 0) {
858
+ if (!url) {
859
+ showToast('无效的视频链接', 'error');
860
+ return;
861
+ }
862
+
863
+ // 保存当前状态到localStorage,让播放页面可以获取
864
+ localStorage.setItem('currentVideoTitle', currentVideoTitle);
865
+ localStorage.setItem('currentEpisodeIndex', episodeIndex);
866
+ localStorage.setItem('currentEpisodes', JSON.stringify(currentEpisodes));
867
+ localStorage.setItem('episodesReversed', episodesReversed);
868
+
869
+ // 构建播放页面URL,传递必要参数
870
+ const playerUrl = `player.html?url=${encodeURIComponent(url)}&title=${encodeURIComponent(vod_name)}&index=${episodeIndex}`;
871
+
872
+ // 在新标签页中打开播放页面
873
+ window.open(playerUrl, '_blank');
874
+ }
875
+
876
+ // 播放上一集
877
+ function playPreviousEpisode() {
878
+ if (currentEpisodeIndex > 0) {
879
+ const prevIndex = currentEpisodeIndex - 1;
880
+ const prevUrl = currentEpisodes[prevIndex];
881
+ playVideo(prevUrl, currentVideoTitle, prevIndex);
882
+ }
883
+ }
884
+
885
+ // 播放下一集
886
+ function playNextEpisode() {
887
+ if (currentEpisodeIndex < currentEpisodes.length - 1) {
888
+ const nextIndex = currentEpisodeIndex + 1;
889
+ const nextUrl = currentEpisodes[nextIndex];
890
+ playVideo(nextUrl, currentVideoTitle, nextIndex);
891
+ }
892
+ }
893
+
894
+ // 处理播放器加载错误
895
+ function handlePlayerError() {
896
+ hideLoading();
897
+ showToast('视频播放加载失败,请尝试其他视频源', 'error');
898
+ }
899
+
900
+ // 辅助函数用于渲染剧集按钮(使用当前的排序状态)
901
+ function renderEpisodes(vodName) {
902
+ const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes;
903
+ return episodes.map((episode, index) => {
904
+ // 根据倒序状态计算真实的剧集索引
905
+ const realIndex = episodesReversed ? currentEpisodes.length - 1 - index : index;
906
+ return `
907
+ <button id="episode-${realIndex}" onclick="playVideo('${episode}','${vodName.replace(/"/g, '&quot;')}', ${realIndex})"
908
+ class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors text-center episode-btn">
909
+ 第${realIndex + 1}集
910
+ </button>
911
+ `;
912
+ }).join('');
913
+ }
914
+
915
+ // 切换排序状态的函数
916
+ function toggleEpisodeOrder() {
917
+ episodesReversed = !episodesReversed;
918
+ // 重新渲染剧集区域,使用 currentVideoTitle 作为视频标题
919
+ const episodesGrid = document.getElementById('episodesGrid');
920
+ if (episodesGrid) {
921
+ episodesGrid.innerHTML = renderEpisodes(currentVideoTitle);
922
+ }
923
+
924
+ // 更新按钮文本和箭头方向
925
+ const toggleBtn = document.querySelector('button[onclick="toggleEpisodeOrder()"]');
926
+ if (toggleBtn) {
927
+ toggleBtn.querySelector('span').textContent = episodesReversed ? '正序排列' : '倒序排列';
928
+ const arrowIcon = toggleBtn.querySelector('svg');
929
+ if (arrowIcon) {
930
+ arrowIcon.style.transform = episodesReversed ? 'rotate(180deg)' : 'rotate(0deg)';
931
+ }
932
+ }
933
+ }
js/config.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 全局常量配置
2
+
3
+ const PROXY_URL = 'https://cors.zme.ink/';
4
+ const HOPLAYER_URL = 'https://hoplayer.com/index.html';
5
+ const SEARCH_HISTORY_KEY = 'videoSearchHistory';
6
+ const MAX_HISTORY_ITEMS = 5;
7
+
8
+ // 网站信息配置
9
+ const SITE_CONFIG = {
10
+ name: 'LibreTV',
11
+ url: 'https://libretv.is-an.org',
12
+ description: '免费在线视频搜索与观看平台',
13
+ logo: 'https://images.icon-icons.com/38/PNG/512/retrotv_5520.png',
14
+ version: '1.0.0'
15
+ };
16
+
17
+ // API站点配置
18
+ const API_SITES = {
19
+ heimuer: {
20
+ api: 'https://json.heimuer.xyz',
21
+ name: '黑木耳',
22
+ detail: 'https://heimuer.tv'
23
+ },
24
+ ffzy: {
25
+ api: 'http://ffzy5.tv',
26
+ name: '非凡影视',
27
+ detail: 'http://ffzy5.tv'
28
+ },
29
+ tyyszy: {
30
+ api: 'https://tyyszy.com',
31
+ name: '天涯资源',
32
+ },
33
+ ckzy: {
34
+ api: 'https://www.ckzy1.com',
35
+ name: 'CK资源',
36
+ adult: true
37
+ },
38
+ zy360: {
39
+ api: 'https://360zy.com',
40
+ name: '360资源',
41
+ },
42
+ wolong: {
43
+ api: 'https://wolongzyw.com',
44
+ name: '卧龙资源',
45
+ },
46
+ cjhw: {
47
+ api: 'https://cjhwba.com',
48
+ name: '新华为',
49
+ },
50
+ jisu: {
51
+ api: 'https://jszyapi.com',
52
+ name: '极速资源',
53
+ detail: 'https://jszyapi.com'
54
+ },
55
+ dbzy: {
56
+ api: 'https://dbzy.com',
57
+ name: '豆瓣资源',
58
+ },
59
+ bfzy: {
60
+ api: 'https://bfzyapi.com',
61
+ name: '暴风资源',
62
+ },
63
+ mozhua: {
64
+ api: 'https://mozhuazy.com',
65
+ name: '魔爪资源',
66
+ },
67
+ mdzy: {
68
+ api: 'https://www.mdzyapi.com',
69
+ name: '魔都资源',
70
+ },
71
+ ruyi: {
72
+ api: 'https://cj.rycjapi.com',
73
+ name: '如意资源',
74
+ },
75
+
76
+ jkun: {
77
+ api: 'https://jkunzyapi.com',
78
+ name: 'jkun资源',
79
+ adult: true
80
+ },
81
+ bwzy: {
82
+ api: 'https://api.bwzym3u8.com',
83
+ name: '百万资源',
84
+ adult: true
85
+ },
86
+ souav: {
87
+ api: 'https://api.souavzy.vip',
88
+ name: 'souav资源',
89
+ adult: true
90
+ },
91
+ siwa: {
92
+ api: 'https://siwazyw.tv',
93
+ name: '丝袜资源',
94
+ adult: true
95
+ },
96
+ r155: {
97
+ api: 'https://155api.com',
98
+ name: '155资源',
99
+ adult: true
100
+ },
101
+ lsb: {
102
+ api: 'https://apilsbzy1.com',
103
+ name: 'lsb资源',
104
+ adult: true
105
+ },
106
+ huangcang: {
107
+ api: 'https://hsckzy.vip',
108
+ name: '黄色仓库',
109
+ adult: true,
110
+ detail: 'https://hsckzy.vip' // 添加detail URL以便特殊处理
111
+ }
112
+ };
113
+
114
+ // 添加聚合搜索的配置选项
115
+ const AGGREGATED_SEARCH_CONFIG = {
116
+ enabled: true, // 是否启用聚合搜索
117
+ timeout: 8000, // 单个源超时时间(毫秒)
118
+ maxResults: 10000, // 最大结果数量
119
+ parallelRequests: true, // 是否并行请求所有源
120
+ showSourceBadges: true // 是否显示来源徽章
121
+ };
122
+
123
+ // 抽象API请求配置
124
+ const API_CONFIG = {
125
+ search: {
126
+ // 修改搜索接口为返回更多详细数据(包括视频封面、简介和播放列表)
127
+ path: '/api.php/provide/vod/?ac=videolist&wd=',
128
+ headers: {
129
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
130
+ 'Accept': 'application/json'
131
+ }
132
+ },
133
+ detail: {
134
+ // 修改详情接口也使用videolist接口,但是通过ID查询,减少请求次数
135
+ path: '/api.php/provide/vod/?ac=videolist&ids=',
136
+ headers: {
137
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
138
+ 'Accept': 'application/json'
139
+ }
140
+ }
141
+ };
142
+
143
+ // 优化后的正则表达式模式
144
+ const M3U8_PATTERN = /\$https?:\/\/[^"'\s]+?\.m3u8/g;
145
+
146
+ // 添加自定义播放器URL
147
+ const CUSTOM_PLAYER_URL = 'player.html'; // 使用相对路径引用本地player.html
148
+
149
+ // 增加视频播放相关配置
150
+ const PLAYER_CONFIG = {
151
+ autoplay: true,
152
+ allowFullscreen: true,
153
+ width: '100%',
154
+ height: '600',
155
+ timeout: 15000, // 播放器加载超时时间
156
+ filterAds: true, // 是否启用广告过滤
157
+ autoPlayNext: true, // 默认启用自动连播功能
158
+ adFilteringEnabled: true, // 默认开启分片广告过滤
159
+ adFilteringStorage: 'adFilteringEnabled' // 存储广告过滤设置的键名
160
+ };
161
+
162
+ // 增加错误信息本地化
163
+ const ERROR_MESSAGES = {
164
+ NETWORK_ERROR: '网络连接错误,请检查网络设置',
165
+ TIMEOUT_ERROR: '请求超时,服务器响应时间过长',
166
+ API_ERROR: 'API接口返回错误,请尝试更换数据源',
167
+ PLAYER_ERROR: '播放器加载失败,请尝试其他视频源',
168
+ UNKNOWN_ERROR: '发生未知错误,请刷新页面重试'
169
+ };
170
+
171
+ // 添加进一步安全设置
172
+ const SECURITY_CONFIG = {
173
+ enableXSSProtection: true, // 是否启用XSS��护
174
+ sanitizeUrls: true, // 是否清理URL
175
+ maxQueryLength: 100, // 最大搜索长度
176
+ allowedApiDomains: [ // 允许的API域名
177
+ 'heimuer.xyz',
178
+ 'ffzy5.tv'
179
+ ]
180
+ };
181
+
182
+ // 添加多个自定义API源的配置
183
+ const CUSTOM_API_CONFIG = {
184
+ separator: ',', // 分隔符
185
+ maxSources: 5, // 最大允许的自定义源数量
186
+ testTimeout: 5000, // 测试超时时间(毫秒)
187
+ namePrefix: 'Custom-', // 自定义源名称前缀
188
+ validateUrl: true, // 验证URL格式
189
+ cacheResults: true, // 缓存测试结果
190
+ cacheExpiry: 5184000000, // 缓存过期时间(2个月)
191
+ adultPropName: 'isAdult' // 用于标记成人内容的属性名
192
+ };
193
+
194
+ // 新增隐藏内置黄色采集站API的变量,默认为true
195
+ const HIDE_BUILTIN_ADULT_APIS = true;
js/ui.js ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // UI相关函数
2
+ function toggleSettings(e) {
3
+ // 阻止事件冒泡,防止触发document的点击事件
4
+ e && e.stopPropagation();
5
+ const panel = document.getElementById('settingsPanel');
6
+ panel.classList.toggle('show');
7
+ }
8
+
9
+ // 改进的Toast显示函数 - 支持队列显示多个Toast
10
+ const toastQueue = [];
11
+ let isShowingToast = false;
12
+
13
+ function showToast(message, type = 'error') {
14
+ // 将新的toast添加到队列
15
+ toastQueue.push({ message, type });
16
+
17
+ // 如果当前没有显示中的toast,则开始显示
18
+ if (!isShowingToast) {
19
+ showNextToast();
20
+ }
21
+ }
22
+
23
+ function showNextToast() {
24
+ if (toastQueue.length === 0) {
25
+ isShowingToast = false;
26
+ return;
27
+ }
28
+
29
+ isShowingToast = true;
30
+ const { message, type } = toastQueue.shift();
31
+
32
+ const toast = document.getElementById('toast');
33
+ const toastMessage = document.getElementById('toastMessage');
34
+
35
+ const bgColors = {
36
+ 'error': 'bg-red-500',
37
+ 'success': 'bg-green-500',
38
+ 'info': 'bg-blue-500',
39
+ 'warning': 'bg-yellow-500'
40
+ };
41
+
42
+ const bgColor = bgColors[type] || bgColors.error;
43
+ toast.className = `fixed top-4 left-1/2 -translate-x-1/2 px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 ${bgColor} text-white`;
44
+ toastMessage.textContent = message;
45
+
46
+ // 显示提示
47
+ toast.style.opacity = '1';
48
+ toast.style.transform = 'translateX(-50%) translateY(0)';
49
+
50
+ // 3秒后自动隐藏
51
+ setTimeout(() => {
52
+ toast.style.opacity = '0';
53
+ toast.style.transform = 'translateX(-50%) translateY(-100%)';
54
+
55
+ // 等待动画完成后显示下一个toast
56
+ setTimeout(() => {
57
+ showNextToast();
58
+ }, 300);
59
+ }, 3000);
60
+ }
61
+
62
+ // 添加显示/隐藏 loading 的函数
63
+ let loadingTimeoutId = null;
64
+
65
+ function showLoading(message = '加载中...') {
66
+ // 清除任何现有的超时
67
+ if (loadingTimeoutId) {
68
+ clearTimeout(loadingTimeoutId);
69
+ }
70
+
71
+ const loading = document.getElementById('loading');
72
+ const messageEl = loading.querySelector('p');
73
+ messageEl.textContent = message;
74
+ loading.style.display = 'flex';
75
+
76
+ // 设置30秒后自动关闭loading,防止无限loading
77
+ loadingTimeoutId = setTimeout(() => {
78
+ hideLoading();
79
+ showToast('操作超时,请稍后重试', 'warning');
80
+ }, 30000);
81
+ }
82
+
83
+ function hideLoading() {
84
+ // 清除超时
85
+ if (loadingTimeoutId) {
86
+ clearTimeout(loadingTimeoutId);
87
+ loadingTimeoutId = null;
88
+ }
89
+
90
+ const loading = document.getElementById('loading');
91
+ loading.style.display = 'none';
92
+ }
93
+
94
+ function updateSiteStatus(isAvailable) {
95
+ const statusEl = document.getElementById('siteStatus');
96
+ if (isAvailable) {
97
+ statusEl.innerHTML = '<span class="text-green-500">●</span> 可用';
98
+ } else {
99
+ statusEl.innerHTML = '<span class="text-red-500">●</span> 不可用';
100
+ }
101
+ }
102
+
103
+ function closeModal() {
104
+ document.getElementById('modal').classList.add('hidden');
105
+ // 清除 iframe 内容
106
+ document.getElementById('modalContent').innerHTML = '';
107
+ }
108
+
109
+ // 获取搜索历史的增强版本 - 支持新旧格式
110
+ function getSearchHistory() {
111
+ try {
112
+ const data = localStorage.getItem(SEARCH_HISTORY_KEY);
113
+ if (!data) return [];
114
+
115
+ const parsed = JSON.parse(data);
116
+
117
+ // 检查是否是数组
118
+ if (!Array.isArray(parsed)) return [];
119
+
120
+ // 支持旧格式(字符串数组)和新格式(对象数组)
121
+ return parsed.map(item => {
122
+ if (typeof item === 'string') {
123
+ return { text: item, timestamp: 0 };
124
+ }
125
+ return item;
126
+ }).filter(item => item && item.text);
127
+ } catch (e) {
128
+ console.error('获取搜索历史出错:', e);
129
+ return [];
130
+ }
131
+ }
132
+
133
+ // 保存搜索历史的增强版本 - 添加时间戳和最大数量限制,现在缓存2个月
134
+ function saveSearchHistory(query) {
135
+ if (!query || !query.trim()) return;
136
+
137
+ // 清理输入,防止XSS
138
+ query = query.trim().substring(0, 50).replace(/</g, '&lt;').replace(/>/g, '&gt;');
139
+
140
+ let history = getSearchHistory();
141
+
142
+ // 获取当前时间
143
+ const now = Date.now();
144
+
145
+ // 过滤掉超过2个月的记录(约60天,60*24*60*60*1000 = 5184000000毫秒)
146
+ history = history.filter(item =>
147
+ typeof item === 'object' && item.timestamp && (now - item.timestamp < 5184000000)
148
+ );
149
+
150
+ // 删除已存在的相同项
151
+ history = history.filter(item =>
152
+ typeof item === 'object' ? item.text !== query : item !== query
153
+ );
154
+
155
+ // 新项添加到开头,包含时间戳
156
+ history.unshift({
157
+ text: query,
158
+ timestamp: now
159
+ });
160
+
161
+ // 限制历史记录数量
162
+ if (history.length > MAX_HISTORY_ITEMS) {
163
+ history = history.slice(0, MAX_HISTORY_ITEMS);
164
+ }
165
+
166
+ try {
167
+ localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history));
168
+ } catch (e) {
169
+ console.error('保存搜索历史失败:', e);
170
+ // 如果存储失败(可能是localStorage已满),尝试清理旧数据
171
+ try {
172
+ localStorage.removeItem(SEARCH_HISTORY_KEY);
173
+ localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history.slice(0, 3)));
174
+ } catch (e2) {
175
+ console.error('再次保存搜索历史失败:', e2);
176
+ }
177
+ }
178
+
179
+ renderSearchHistory();
180
+ }
181
+
182
+ // 渲染最近搜索历史的增强版本
183
+ function renderSearchHistory() {
184
+ const historyContainer = document.getElementById('recentSearches');
185
+ if (!historyContainer) return;
186
+
187
+ const history = getSearchHistory();
188
+
189
+ if (history.length === 0) {
190
+ historyContainer.innerHTML = '';
191
+ return;
192
+ }
193
+
194
+ // 创建一个包含标题和清除按钮的行
195
+ historyContainer.innerHTML = `
196
+ <div class="flex justify-between items-center w-full mb-2">
197
+ <div class="text-gray-500">最近搜索:</div>
198
+ <button id="clearHistoryBtn" class="text-gray-500 hover:text-white transition-colors"
199
+ onclick="clearSearchHistory()" aria-label="清除搜索历史">
200
+ 清除搜索历史
201
+ </button>
202
+ </div>
203
+ `;
204
+
205
+ history.forEach(item => {
206
+ const tag = document.createElement('button');
207
+ tag.className = 'search-tag';
208
+ tag.textContent = item.text;
209
+
210
+ // 添加时间提示(如果有时间戳)
211
+ if (item.timestamp) {
212
+ const date = new Date(item.timestamp);
213
+ tag.title = `搜索于: ${date.toLocaleString()}`;
214
+ }
215
+
216
+ tag.onclick = function() {
217
+ document.getElementById('searchInput').value = item.text;
218
+ search();
219
+ };
220
+ historyContainer.appendChild(tag);
221
+ });
222
+ }
223
+
224
+ // 增加清除搜索历史功能
225
+ function clearSearchHistory() {
226
+ try {
227
+ localStorage.removeItem(SEARCH_HISTORY_KEY);
228
+ renderSearchHistory();
229
+ showToast('搜索历史已清除', 'success');
230
+ } catch (e) {
231
+ console.error('清除搜索历史失败:', e);
232
+ showToast('清除搜索历史失败', 'error');
233
+ }
234
+ }
player.html ADDED
@@ -0,0 +1,947 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LibreTV 播放器</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="css/styles.css">
9
+ <style>
10
+ body, html {
11
+ margin: 0;
12
+ padding: 0;
13
+ width: 100%;
14
+ height: 100%;
15
+ background-color: #0f1622;
16
+ color: white;
17
+ }
18
+ .player-container {
19
+ width: 100%;
20
+ max-width: 1200px;
21
+ margin: 0 auto;
22
+ }
23
+ #player {
24
+ width: 100%;
25
+ height: 60vh; /* 视频播放器高度 */
26
+ }
27
+ .loading-container {
28
+ position: absolute;
29
+ top: 0;
30
+ left: 0;
31
+ right: 0;
32
+ bottom: 0;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ background-color: rgba(0, 0, 0, 0.7);
37
+ color: white;
38
+ z-index: 100;
39
+ flex-direction: column;
40
+ }
41
+ .loading-spinner {
42
+ width: 50px;
43
+ height: 50px;
44
+ border: 4px solid rgba(255, 255, 255, 0.3);
45
+ border-radius: 50%;
46
+ border-top-color: white;
47
+ animation: spin 1s ease-in-out infinite;
48
+ margin-bottom: 10px;
49
+ }
50
+ @keyframes spin {
51
+ to { transform: rotate(360deg); }
52
+ }
53
+ .error-container {
54
+ position: absolute;
55
+ top: 0;
56
+ left: 0;
57
+ right: 0;
58
+ bottom: 0;
59
+ display: none;
60
+ align-items: center;
61
+ justify-content: center;
62
+ background-color: rgba(0, 0, 0, 0.7);
63
+ color: white;
64
+ z-index: 100;
65
+ flex-direction: column;
66
+ text-align: center;
67
+ padding: 1rem;
68
+ }
69
+ .error-icon {
70
+ font-size: 48px;
71
+ margin-bottom: 10px;
72
+ }
73
+ .episode-active {
74
+ background-color: #3b82f6 !important;
75
+ border-color: #60a5fa !important;
76
+ }
77
+ .episode-grid {
78
+ max-height: 30vh;
79
+ overflow-y: auto;
80
+ padding: 1rem 0;
81
+ }
82
+ .switch {
83
+ position: relative;
84
+ display: inline-block;
85
+ width: 46px;
86
+ height: 24px;
87
+ }
88
+ .switch input {
89
+ opacity: 0;
90
+ width: 0;
91
+ height: 0;
92
+ }
93
+ .slider {
94
+ position: absolute;
95
+ cursor: pointer;
96
+ top: 0;
97
+ left: 0;
98
+ right: 0;
99
+ bottom: 0;
100
+ background-color: #333;
101
+ transition: .4s;
102
+ border-radius: 24px;
103
+ }
104
+ .slider:before {
105
+ position: absolute;
106
+ content: "";
107
+ height: 18px;
108
+ width: 18px;
109
+ left: 3px;
110
+ bottom: 3px;
111
+ background-color: white;
112
+ transition: .4s;
113
+ border-radius: 50%;
114
+ }
115
+ input:checked + .slider {
116
+ background-color: #00ccff;
117
+ }
118
+ input:checked + .slider:before {
119
+ transform: translateX(22px);
120
+ }
121
+ /* 添加快捷键提示样式 */
122
+ .shortcut-hint {
123
+ position: fixed;
124
+ top: 50%;
125
+ left: 50%;
126
+ transform: translate(-50%, -50%);
127
+ background-color: rgba(0, 0, 0, 0.8);
128
+ color: white;
129
+ padding: 1rem 2rem;
130
+ border-radius: 0.5rem;
131
+ font-size: 1.5rem;
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 0.5rem;
135
+ z-index: 1000;
136
+ opacity: 0;
137
+ transition: opacity 0.3s ease;
138
+ }
139
+ .shortcut-hint.show {
140
+ opacity: 1;
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <header class="bg-[#111] p-4 flex justify-between items-center border-b border-[#333]">
146
+ <div class="flex items-center">
147
+ <a href="index.html" class="flex items-center">
148
+ <svg class="w-8 h-8 mr-2 text-[#00ccff]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
149
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
150
+ </svg>
151
+ <h1 class="text-xl font-bold gradient-text">LibreTV</h1>
152
+ </a>
153
+ </div>
154
+ <h2 id="videoTitle" class="text-xl font-semibold truncate flex-1 text-center"></h2>
155
+ <a href="index.html" class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors">
156
+ 返回首页
157
+ </a>
158
+ </header>
159
+
160
+ <main class="container mx-auto px-4 py-4">
161
+ <!-- 视频播放区 -->
162
+ <div class="player-container">
163
+ <div class="relative">
164
+ <div id="player"></div>
165
+ <div class="loading-container" id="loading">
166
+ <div class="loading-spinner"></div>
167
+ <div>正在加载视频...</div>
168
+ </div>
169
+ <div class="error-container" id="error">
170
+ <div class="error-icon">⚠️</div>
171
+ <div id="error-message">视频加载失败</div>
172
+ <div style="margin-top: 10px; font-size: 14px; color: #aaa;">请尝试其他视频源或稍后重试</div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- 集数导航 -->
178
+ <div class="player-container">
179
+ <div class="flex justify-between items-center my-4">
180
+ <button onclick="playPreviousEpisode()" id="prevButton" class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors">
181
+ <svg class="w-5 h-5 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
182
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
183
+ </svg>
184
+ 上一集
185
+ </button>
186
+ <span class="text-gray-400" id="episodeInfo">加载中...</span>
187
+ <button onclick="playNextEpisode()" id="nextButton" class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors">
188
+ 下一集
189
+ <svg class="w-5 h-5 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
190
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
191
+ </svg>
192
+ </button>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- 添加自动播放开关和排序按钮 -->
197
+ <div class="player-container">
198
+ <div class="flex justify-end items-center mb-4 gap-2">
199
+ <span class="text-gray-400 text-sm">自动连播</span>
200
+ <label class="switch">
201
+ <input type="checkbox" id="autoplayToggle">
202
+ <span class="slider"></span>
203
+ </label>
204
+ <button onclick="toggleEpisodeOrder()" class="ml-4 px-4 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-semibold rounded-full shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center space-x-2">
205
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" id="orderIcon" viewBox="0 0 20 20" fill="currentColor">
206
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v3.586L7.707 9.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 10.586V7z" clip-rule="evenodd" />
207
+ </svg>
208
+ <span id="orderText">倒序排列</span>
209
+ </button>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- 集数网格 -->
214
+ <div class="player-container">
215
+ <div class="episode-grid" id="episodesGrid">
216
+ <div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2" id="episodesList">
217
+ <!-- 集数将在这里动态加载 -->
218
+ <div class="col-span-full text-center text-gray-400 py-8">加载中...</div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </main>
223
+
224
+ <!-- 添加快捷键提示元素 -->
225
+ <div class="shortcut-hint" id="shortcutHint">
226
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" id="shortcutIcon">
227
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
228
+ </svg>
229
+ <span id="shortcutText"></span>
230
+ </div>
231
+
232
+ <script src="https://s4.zstatic.net/ajax/libs/hls.js/1.5.6/hls.min.js" integrity="sha256-X1GmLMzVcTBRiGjEau+gxGpjRK96atNczcLBg5w6hKA=" crossorigin="anonymous"></script>
233
+ <script src="https://s4.zstatic.net/ajax/libs/dplayer/1.26.0/DPlayer.min.js" integrity="sha256-OJg03lDZP0NAcl3waC9OT5jEa8XZ8SM2n081Ik953o4=" crossorigin="anonymous"></script>
234
+ <script src="js/config.js"></script>
235
+ <script>
236
+ // 全局变量
237
+ let currentVideoTitle = '';
238
+ let currentEpisodeIndex = 0;
239
+ let currentEpisodes = [];
240
+ let episodesReversed = false;
241
+ let dp = null;
242
+ let currentHls = null; // 跟踪当前HLS实例
243
+ let autoplayEnabled = true; // 默认开启自动连播
244
+ let isUserSeeking = false; // 跟踪用户是否正在拖动进度条
245
+ let videoHasEnded = false; // 跟踪视频是否已经自然结束
246
+ let userClickedPosition = null; // 记录用户点击的位置
247
+ let shortcutHintTimeout = null; // 用于控制快捷键提示显示时间
248
+ let adFilteringEnabled = true; // 默认开启广告过滤
249
+
250
+ // 页面加载
251
+ document.addEventListener('DOMContentLoaded', function() {
252
+ // 解析URL参数
253
+ const urlParams = new URLSearchParams(window.location.search);
254
+ const videoUrl = urlParams.get('url');
255
+ const title = urlParams.get('title');
256
+ const index = parseInt(urlParams.get('index') || '0');
257
+
258
+ // 从localStorage获取数据
259
+ currentVideoTitle = title || localStorage.getItem('currentVideoTitle') || '未知视频';
260
+ currentEpisodeIndex = index;
261
+
262
+ // 设置自动连播开关状态
263
+ autoplayEnabled = localStorage.getItem('autoplayEnabled') !== 'false'; // 默认为true
264
+ document.getElementById('autoplayToggle').checked = autoplayEnabled;
265
+
266
+ // 获取广告过滤设置
267
+ adFilteringEnabled = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; // 默认为true
268
+
269
+ // 监听自动连播开关变化
270
+ document.getElementById('autoplayToggle').addEventListener('change', function(e) {
271
+ autoplayEnabled = e.target.checked;
272
+ localStorage.setItem('autoplayEnabled', autoplayEnabled);
273
+ });
274
+
275
+ try {
276
+ currentEpisodes = JSON.parse(localStorage.getItem('currentEpisodes') || '[]');
277
+ episodesReversed = localStorage.getItem('episodesReversed') === 'true';
278
+ } catch (e) {
279
+ console.error('获取集数信息失败:', e);
280
+ currentEpisodes = [];
281
+ episodesReversed = false;
282
+ }
283
+
284
+ // 设置页面标题
285
+ document.title = currentVideoTitle + ' - LibreTV播放器';
286
+ document.getElementById('videoTitle').textContent = currentVideoTitle;
287
+
288
+ // 初始化播放器
289
+ if (videoUrl) {
290
+ initPlayer(videoUrl);
291
+ } else {
292
+ showError('无效的视频链接');
293
+ }
294
+
295
+ // 更新集数信息
296
+ updateEpisodeInfo();
297
+
298
+ // 渲染集数列表
299
+ renderEpisodes();
300
+
301
+ // 更新按钮状态
302
+ updateButtonStates();
303
+
304
+ // 更新排序按钮状态
305
+ updateOrderButton();
306
+
307
+ // 添加对进度条的监听,确保点击准确跳转
308
+ setTimeout(() => {
309
+ setupProgressBarPreciseClicks();
310
+ }, 1000);
311
+
312
+ // 添加键盘快捷键事件监听
313
+ document.addEventListener('keydown', handleKeyboardShortcuts);
314
+ });
315
+
316
+ // 处理键盘快捷键
317
+ function handleKeyboardShortcuts(e) {
318
+ // 忽略输入框中的按键事件
319
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
320
+
321
+ // Alt + 左箭头 = 上一集
322
+ if (e.altKey && e.key === 'ArrowLeft') {
323
+ if (currentEpisodeIndex > 0) {
324
+ playPreviousEpisode();
325
+ showShortcutHint('上一集', 'left');
326
+ e.preventDefault();
327
+ }
328
+ }
329
+
330
+ // Alt + 右箭头 = 下一集
331
+ if (e.altKey && e.key === 'ArrowRight') {
332
+ if (currentEpisodeIndex < currentEpisodes.length - 1) {
333
+ playNextEpisode();
334
+ showShortcutHint('下一集', 'right');
335
+ e.preventDefault();
336
+ }
337
+ }
338
+ }
339
+
340
+ // 显示快捷键提示
341
+ function showShortcutHint(text, direction) {
342
+ const hintElement = document.getElementById('shortcutHint');
343
+ const textElement = document.getElementById('shortcutText');
344
+ const iconElement = document.getElementById('shortcutIcon');
345
+
346
+ // 清除之前的超时
347
+ if (shortcutHintTimeout) {
348
+ clearTimeout(shortcutHintTimeout);
349
+ }
350
+
351
+ // 设置文本和图标方向
352
+ textElement.textContent = text;
353
+
354
+ if (direction === 'left') {
355
+ iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>';
356
+ } else {
357
+ iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>';
358
+ }
359
+
360
+ // 显示提示
361
+ hintElement.classList.add('show');
362
+
363
+ // 两秒后隐藏
364
+ shortcutHintTimeout = setTimeout(() => {
365
+ hintElement.classList.remove('show');
366
+ }, 2000);
367
+ }
368
+
369
+ // 初始化播放器
370
+ function initPlayer(videoUrl) {
371
+ if (!videoUrl) return;
372
+
373
+ // 配置HLS.js选项
374
+ const hlsConfig = {
375
+ debug: false,
376
+ loader: adFilteringEnabled ? CustomHlsJsLoader : Hls.DefaultConfig.loader,
377
+ enableWorker: true,
378
+ lowLatencyMode: false,
379
+ backBufferLength: 90,
380
+ maxBufferLength: 30,
381
+ maxMaxBufferLength: 60,
382
+ maxBufferSize: 30 * 1000 * 1000,
383
+ maxBufferHole: 0.5,
384
+ fragLoadingMaxRetry: 6,
385
+ fragLoadingMaxRetryTimeout: 64000,
386
+ fragLoadingRetryDelay: 1000,
387
+ manifestLoadingMaxRetry: 3,
388
+ manifestLoadingRetryDelay: 1000,
389
+ levelLoadingMaxRetry: 4,
390
+ levelLoadingRetryDelay: 1000,
391
+ startLevel: -1,
392
+ abrEwmaDefaultEstimate: 500000,
393
+ abrBandWidthFactor: 0.95,
394
+ abrBandWidthUpFactor: 0.7,
395
+ abrMaxWithRealBitrate: true,
396
+ stretchShortVideoTrack: true,
397
+ appendErrorMaxRetry: 5, // 增加尝试次数
398
+ liveSyncDurationCount: 3,
399
+ liveDurationInfinity: false
400
+ };
401
+
402
+ // 创建DPlayer实例
403
+ dp = new DPlayer({
404
+ container: document.getElementById('player'),
405
+ autoplay: true,
406
+ theme: '#00ccff',
407
+ preload: 'auto',
408
+ loop: false,
409
+ lang: 'zh-cn',
410
+ hotkey: true, // 启用键盘控制,包括空格暂停/播放、方向键控制进度和音量
411
+ mutex: true,
412
+ volume: 0.7,
413
+ screenshot: true, // 启用截图功能
414
+ preventClickToggle: false, // 允许点击视频切换播放/暂停
415
+ airplay: true, // 在Safari中启用AirPlay功能
416
+ chromecast: true, // 启用Chromecast投屏功能
417
+ contextmenu: [ // 自定义右键菜单
418
+ {
419
+ text: '关于 LibreTV',
420
+ link: 'https://github.com/bestzwei/LibreTV'
421
+ },
422
+ {
423
+ text: '问题反馈',
424
+ click: (player) => {
425
+ window.open('https://github.com/bestzwei/LibreTV/issues', '_blank');
426
+ }
427
+ }
428
+ ],
429
+ video: {
430
+ url: videoUrl,
431
+ type: 'hls',
432
+ pic: 'https://img.picgo.net/2025/04/12/image362e7d38b4af4a74.png', // 设置视频封面图
433
+ customType: {
434
+ hls: function(video, player) {
435
+ // 清理之前的HLS实例
436
+ if (currentHls && currentHls.destroy) {
437
+ try {
438
+ currentHls.destroy();
439
+ } catch (e) {
440
+ console.warn('销毁旧HLS实例出错:', e);
441
+ }
442
+ }
443
+
444
+ // 创建新的HLS实例
445
+ const hls = new Hls(hlsConfig);
446
+ currentHls = hls;
447
+
448
+ // 跟踪是否已经显示错误
449
+ let errorDisplayed = false;
450
+ // 跟踪是否有错误发生
451
+ let errorCount = 0;
452
+ // 跟踪视频是否开始播放
453
+ let playbackStarted = false;
454
+ // 跟踪视频是否出现bufferAppendError
455
+ let bufferAppendErrorCount = 0;
456
+
457
+ // 监听视频播放事件
458
+ video.addEventListener('playing', function() {
459
+ playbackStarted = true;
460
+ document.getElementById('loading').style.display = 'none';
461
+ document.getElementById('error').style.display = 'none';
462
+ });
463
+
464
+ // 监听视频进度事件
465
+ video.addEventListener('timeupdate', function() {
466
+ if (video.currentTime > 1) {
467
+ // 视频进度超过1秒,隐藏错误(如果存在)
468
+ document.getElementById('error').style.display = 'none';
469
+ }
470
+ });
471
+
472
+ hls.loadSource(video.src);
473
+ hls.attachMedia(video);
474
+
475
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {
476
+ video.play().catch(e => {
477
+ console.warn('自动播放被阻止:', e);
478
+ });
479
+ });
480
+
481
+ hls.on(Hls.Events.ERROR, function(event, data) {
482
+ console.log('HLS事件:', event, '数据:', data);
483
+
484
+ // 增加错误计数
485
+ errorCount++;
486
+
487
+ // 处理bufferAppendError
488
+ if (data.details === 'bufferAppendError') {
489
+ bufferAppendErrorCount++;
490
+ console.warn(`bufferAppendError 发生 ${bufferAppendErrorCount} 次`);
491
+
492
+ // 如果视频已经开始播放,则忽略这个错误
493
+ if (playbackStarted) {
494
+ console.log('视频已在播放中,忽略bufferAppendError');
495
+ return;
496
+ }
497
+
498
+ // 如果出现多次bufferAppendError但视频未播放,尝试恢复
499
+ if (bufferAppendErrorCount >= 3) {
500
+ hls.recoverMediaError();
501
+ }
502
+ }
503
+
504
+ // 如果是致命错误,且视频未播放
505
+ if (data.fatal && !playbackStarted) {
506
+ console.error('致命HLS错误:', data);
507
+
508
+ // 尝试恢复错误
509
+ switch(data.type) {
510
+ case Hls.ErrorTypes.NETWORK_ERROR:
511
+ console.log("尝试恢复网络错误");
512
+ hls.startLoad();
513
+ break;
514
+ case Hls.ErrorTypes.MEDIA_ERROR:
515
+ console.log("尝试恢复媒体错误");
516
+ hls.recoverMediaError();
517
+ break;
518
+ default:
519
+ // 仅在多次恢复尝试后显示错误
520
+ if (errorCount > 3 && !errorDisplayed) {
521
+ errorDisplayed = true;
522
+ showError('视频加载失败,可能是格式不兼容或源不可用');
523
+ }
524
+ break;
525
+ }
526
+ }
527
+ });
528
+
529
+ // 监听分段加载事件
530
+ hls.on(Hls.Events.FRAG_LOADED, function() {
531
+ document.getElementById('loading').style.display = 'none';
532
+ });
533
+
534
+ // 监听级别加载事件
535
+ hls.on(Hls.Events.LEVEL_LOADED, function() {
536
+ document.getElementById('loading').style.display = 'none';
537
+ });
538
+ }
539
+ }
540
+ }
541
+ });
542
+
543
+ dp.on('loadedmetadata', function() {
544
+ document.getElementById('loading').style.display = 'none';
545
+ videoHasEnded = false; // 视频加载时重置结束标志
546
+
547
+ // 视频加载完成后重新设置进度条点击监听
548
+ setupProgressBarPreciseClicks();
549
+ });
550
+
551
+ dp.on('error', function() {
552
+ // 检查视频是否已经在播放
553
+ if (dp.video && dp.video.currentTime > 1) {
554
+ console.log('发生错误,但视频已在播放中,忽略');
555
+ return;
556
+ }
557
+ showError('视频播放失败,请检查视频源或网络连接');
558
+ });
559
+
560
+ // 添加seeking和seeked事件监听器,以检测用户是否在拖动进度条
561
+ dp.on('seeking', function() {
562
+ isUserSeeking = true;
563
+ videoHasEnded = false; // 重置视频结束标志
564
+
565
+ // 如果是用户通过点击进度条设置的位置,确保准确跳转
566
+ if (userClickedPosition !== null && dp.video) {
567
+ // 确保用户的点击位置被正确应用,避免自动跳至视频末尾
568
+ const clickedTime = userClickedPosition;
569
+
570
+ // 防止跳转到视频结尾
571
+ if (Math.abs(dp.video.duration - clickedTime) < 0.5) {
572
+ // 如果点击的位置非常接近结尾,稍微减少一点时间
573
+ dp.video.currentTime = Math.max(0, clickedTime - 0.5);
574
+ } else {
575
+ dp.video.currentTime = clickedTime;
576
+ }
577
+
578
+ // 清除记录的位置
579
+ setTimeout(() => {
580
+ userClickedPosition = null;
581
+ }, 200);
582
+ }
583
+ });
584
+
585
+ // 改进seeked事件处理
586
+ dp.on('seeked', function() {
587
+ // 如果视频跳转到了非常接近结尾的位置(小于0.3秒),且不是自然播放到此处
588
+ if (dp.video && dp.video.duration > 0) {
589
+ const timeFromEnd = dp.video.duration - dp.video.currentTime;
590
+ if (timeFromEnd < 0.3 && isUserSeeking) {
591
+ // 将播放时间往回移动一点点,避免触发结束事件
592
+ dp.video.currentTime = Math.max(0, dp.video.currentTime - 1);
593
+ }
594
+ }
595
+
596
+ // 延迟重置seeking标志,以便于区分自然播放结束和用户拖拽
597
+ setTimeout(() => {
598
+ isUserSeeking = false;
599
+ }, 200);
600
+ });
601
+
602
+ // 修改视频结束事件监听器,添加额外检查
603
+ dp.on('ended', function() {
604
+ videoHasEnded = true; // 标记视频已自然结束
605
+
606
+ // 如果启用了自动连播,并且有下一集可播放,则自动播放下一集
607
+ if (autoplayEnabled && currentEpisodeIndex < currentEpisodes.length - 1) {
608
+ console.log('视频播放结束,自动播放下一集');
609
+ // 稍长延迟以确保所有事件处理完成
610
+ setTimeout(() => {
611
+ // 确认不是因为用户拖拽导致的假结束事件
612
+ if (videoHasEnded && !isUserSeeking) {
613
+ playNextEpisode();
614
+ videoHasEnded = false; // 重置标志
615
+ }
616
+ }, 1000);
617
+ } else {
618
+ console.log('视频播放结束,无下一集或未启用自动连播');
619
+ }
620
+ });
621
+
622
+ // 添加事件监听以检测近视频末尾的点击拖动
623
+ dp.on('timeupdate', function() {
624
+ if (dp.video && dp.duration > 0) {
625
+ // 如果视频接近结尾但不是自然播放到结尾,重置自然结束标志
626
+ if (isUserSeeking && dp.video.currentTime > dp.video.duration * 0.95) {
627
+ videoHasEnded = false;
628
+ }
629
+ }
630
+ });
631
+
632
+ // 10秒后如果仍在加载,但不立即显示错误
633
+ setTimeout(function() {
634
+ // 如果视频已经播放开始,则不显示错误
635
+ if (dp && dp.video && dp.video.currentTime > 0) {
636
+ return;
637
+ }
638
+
639
+ if (document.getElementById('loading').style.display !== 'none') {
640
+ document.getElementById('loading').innerHTML = `
641
+ <div class="loading-spinner"></div>
642
+ <div>视频加载时间较长,请耐心等待...</div>
643
+ <div style="font-size: 12px; color: #aaa; margin-top: 10px;">如长时间无响应,请尝试其他视频源</div>
644
+ `;
645
+ }
646
+ }, 10000);
647
+ }
648
+
649
+ // 自定义M3U8 Loader用于过滤广告
650
+ class CustomHlsJsLoader extends Hls.DefaultConfig.loader {
651
+ constructor(config) {
652
+ super(config);
653
+ const load = this.load.bind(this);
654
+ this.load = function(context, config, callbacks) {
655
+ // 拦截manifest和level请求
656
+ if (context.type === 'manifest' || context.type === 'level') {
657
+ const onSuccess = callbacks.onSuccess;
658
+ callbacks.onSuccess = function(response, stats, context) {
659
+ // 如果是m3u8文件,处理内容以移除广告分段
660
+ if (response.data && typeof response.data === 'string') {
661
+ // 过滤掉广告段 - 实现更精确的广告过滤逻辑
662
+ response.data = filterAdsFromM3U8(response.data, true);
663
+ }
664
+ return onSuccess(response, stats, context);
665
+ };
666
+ }
667
+ // 执行原始load方法
668
+ load(context, config, callbacks);
669
+ };
670
+ }
671
+ }
672
+
673
+ // M3U8清单广告过滤函数
674
+ function filterAdsFromM3U8(m3u8Content, strictMode = false) {
675
+ if (!m3u8Content) return '';
676
+
677
+ // 按行分割M3U8内容
678
+ const lines = m3u8Content.split('\n');
679
+ const filteredLines = [];
680
+
681
+ for (let i = 0; i < lines.length; i++) {
682
+ const line = lines[i];
683
+
684
+ // 只过滤#EXT-X-DISCONTINUITY标识
685
+ if (!line.includes('#EXT-X-DISCONTINUITY')) {
686
+ filteredLines.push(line);
687
+ }
688
+ }
689
+
690
+ return filteredLines.join('\n');
691
+ }
692
+
693
+ // 显示错误
694
+ function showError(message) {
695
+ // 在视频已经播放的情况下不显示错误
696
+ if (dp && dp.video && dp.video.currentTime > 1) {
697
+ console.log('忽略错误:', message);
698
+ return;
699
+ }
700
+
701
+ document.getElementById('loading').style.display = 'none';
702
+ document.getElementById('error').style.display = 'flex';
703
+ document.getElementById('error-message').textContent = message;
704
+ }
705
+
706
+ // 更新集数信息
707
+ function updateEpisodeInfo() {
708
+ if (currentEpisodes.length > 0) {
709
+ document.getElementById('episodeInfo').textContent = `第 ${currentEpisodeIndex + 1}/${currentEpisodes.length} 集`;
710
+ } else {
711
+ document.getElementById('episodeInfo').textContent = '无集数信息';
712
+ }
713
+ }
714
+
715
+ // 更新按钮状态
716
+ function updateButtonStates() {
717
+ const prevButton = document.getElementById('prevButton');
718
+ const nextButton = document.getElementById('nextButton');
719
+
720
+ // 处理上一集按钮
721
+ if (currentEpisodeIndex > 0) {
722
+ prevButton.classList.remove('bg-gray-700', 'cursor-not-allowed');
723
+ prevButton.classList.add('bg-[#222]', 'hover:bg-[#333]');
724
+ prevButton.removeAttribute('disabled');
725
+ } else {
726
+ prevButton.classList.add('bg-gray-700', 'cursor-not-allowed');
727
+ prevButton.classList.remove('bg-[#222]', 'hover:bg-[#333]');
728
+ prevButton.setAttribute('disabled', '');
729
+ }
730
+
731
+ // 处理下一集按钮
732
+ if (currentEpisodeIndex < currentEpisodes.length - 1) {
733
+ nextButton.classList.remove('bg-gray-700', 'cursor-not-allowed');
734
+ nextButton.classList.add('bg-[#222]', 'hover:bg-[#333]');
735
+ nextButton.removeAttribute('disabled');
736
+ } else {
737
+ nextButton.classList.add('bg-gray-700', 'cursor-not-allowed');
738
+ nextButton.classList.remove('bg-[#222]', 'hover:bg-[#333]');
739
+ nextButton.setAttribute('disabled', '');
740
+ }
741
+ }
742
+
743
+ // 渲染集数按钮
744
+ function renderEpisodes() {
745
+ const episodesList = document.getElementById('episodesList');
746
+ if (!episodesList) return;
747
+
748
+ if (!currentEpisodes || currentEpisodes.length === 0) {
749
+ episodesList.innerHTML = '<div class="col-span-full text-center text-gray-400 py-8">没有可用的集数</div>';
750
+ return;
751
+ }
752
+
753
+ const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes;
754
+ let html = '';
755
+
756
+ episodes.forEach((episode, index) => {
757
+ // 根据倒序状态计算真实的剧集索引
758
+ const realIndex = episodesReversed ? currentEpisodes.length - 1 - index : index;
759
+ const isActive = realIndex === currentEpisodeIndex;
760
+
761
+ html += `
762
+ <button id="episode-${realIndex}"
763
+ onclick="playEpisode(${realIndex})"
764
+ class="px-4 py-2 ${isActive ? 'episode-active' : 'bg-[#222] hover:bg-[#333]'} border ${isActive ? 'border-blue-500' : 'border-[#333]'} rounded-lg transition-colors text-center episode-btn">
765
+ 第${realIndex + 1}集
766
+ </button>
767
+ `;
768
+ });
769
+
770
+ episodesList.innerHTML = html;
771
+ }
772
+
773
+ // 播放指定集数
774
+ function playEpisode(index) {
775
+ if (index < 0 || index >= currentEpisodes.length) return;
776
+
777
+ // 首先隐藏之前可能显示的错误
778
+ document.getElementById('error').style.display = 'none';
779
+ // 显示加载指示器
780
+ document.getElementById('loading').style.display = 'flex';
781
+ document.getElementById('loading').innerHTML = `
782
+ <div class="loading-spinner"></div>
783
+ <div>正在加载视频...</div>
784
+ `;
785
+
786
+ const url = currentEpisodes[index];
787
+ currentEpisodeIndex = index;
788
+ videoHasEnded = false; // 重置视频结束标志
789
+
790
+ // 更新URL,不刷新页面
791
+ const newUrl = new URL(window.location.href);
792
+ newUrl.searchParams.set('index', index);
793
+ newUrl.searchParams.set('url', url);
794
+ window.history.pushState({}, '', newUrl);
795
+
796
+ // 更新播放器
797
+ if (dp) {
798
+ try {
799
+ dp.switchVideo({
800
+ url: url,
801
+ type: 'hls'
802
+ });
803
+
804
+ // 确保播放开始
805
+ const playPromise = dp.play();
806
+ if (playPromise !== undefined) {
807
+ playPromise.catch(error => {
808
+ console.warn('播放失败,尝试重新初始化:', error);
809
+ // 如果切换视频失败,重新初始化播放器
810
+ initPlayer(url);
811
+ });
812
+ }
813
+ } catch (e) {
814
+ console.error('切换视频出错,尝试重新初始化:', e);
815
+ // 如果出错,重新初始化播放器
816
+ initPlayer(url);
817
+ }
818
+ } else {
819
+ initPlayer(url);
820
+ }
821
+
822
+ // 更新UI
823
+ updateEpisodeInfo();
824
+ updateButtonStates();
825
+ renderEpisodes();
826
+
827
+ // 重置用户点击位置记录
828
+ userClickedPosition = null;
829
+ }
830
+
831
+ // 播放上一集
832
+ function playPreviousEpisode() {
833
+ if (currentEpisodeIndex > 0) {
834
+ playEpisode(currentEpisodeIndex - 1);
835
+ }
836
+ }
837
+
838
+ // 播放下一集
839
+ function playNextEpisode() {
840
+ if (currentEpisodeIndex < currentEpisodes.length - 1) {
841
+ playEpisode(currentEpisodeIndex + 1);
842
+ }
843
+ }
844
+
845
+ // 切换集数排序
846
+ function toggleEpisodeOrder() {
847
+ episodesReversed = !episodesReversed;
848
+
849
+ // 保存到localStorage
850
+ localStorage.setItem('episodesReversed', episodesReversed);
851
+
852
+ // 重新渲染集数列表
853
+ renderEpisodes();
854
+
855
+ // 更新排序按钮
856
+ updateOrderButton();
857
+ }
858
+
859
+ // 更新排序按钮状态
860
+ function updateOrderButton() {
861
+ const orderText = document.getElementById('orderText');
862
+ const orderIcon = document.getElementById('orderIcon');
863
+
864
+ if (orderText && orderIcon) {
865
+ orderText.textContent = episodesReversed ? '正序排列' : '倒序排列';
866
+ orderIcon.style.transform = episodesReversed ? 'rotate(180deg)' : '';
867
+ }
868
+ }
869
+
870
+ // 设置进度条准确点击处理
871
+ function setupProgressBarPreciseClicks() {
872
+ // 查找DPlayer的进度条元素
873
+ const progressBar = document.querySelector('.dplayer-bar-wrap');
874
+ if (!progressBar || !dp || !dp.video) return;
875
+
876
+ // 移除可能存在的旧事件监听器
877
+ progressBar.removeEventListener('mousedown', handleProgressBarClick);
878
+
879
+ // 添加新的事件监听器
880
+ progressBar.addEventListener('mousedown', handleProgressBarClick);
881
+
882
+ // 在移动端也添加触摸事件支持
883
+ progressBar.removeEventListener('touchstart', handleProgressBarTouch);
884
+ progressBar.addEventListener('touchstart', handleProgressBarTouch);
885
+
886
+ console.log('进度条精确点击监听器已设置');
887
+ }
888
+
889
+ // 处理进度条点击
890
+ function handleProgressBarClick(e) {
891
+ if (!dp || !dp.video) return;
892
+
893
+ // 计算点击位置相对于进度条的比例
894
+ const rect = e.currentTarget.getBoundingClientRect();
895
+ const percentage = (e.clientX - rect.left) / rect.width;
896
+
897
+ // 计算点击位置对应的视频时间
898
+ const duration = dp.video.duration;
899
+ let clickTime = percentage * duration;
900
+
901
+ // 处理视频接近结尾的情况
902
+ if (duration - clickTime < 1) {
903
+ // 如果点击位置非常接近结尾,稍微往前移一点
904
+ clickTime = Math.min(clickTime, duration - 1.5);
905
+ console.log(`进度条点击接近结尾,调整时间为 ${clickTime.toFixed(2)}/${duration.toFixed(2)}`);
906
+ }
907
+
908
+ // 记录用户点击的位置
909
+ userClickedPosition = clickTime;
910
+
911
+ // 输出调试信息
912
+ console.log(`进度条点击: ${percentage.toFixed(4)}, 时间: ${clickTime.toFixed(2)}/${duration.toFixed(2)}`);
913
+
914
+ // 阻止默认事件传播,避免DPlayer内部逻辑将视频跳至末尾
915
+ e.stopPropagation();
916
+
917
+ // 直接设置视频时间
918
+ dp.seek(clickTime);
919
+ }
920
+
921
+ // 处理移动端触摸事件
922
+ function handleProgressBarTouch(e) {
923
+ if (!dp || !dp.video || !e.touches[0]) return;
924
+
925
+ const touch = e.touches[0];
926
+ const rect = e.currentTarget.getBoundingClientRect();
927
+ const percentage = (touch.clientX - rect.left) / rect.width;
928
+
929
+ const duration = dp.video.duration;
930
+ let clickTime = percentage * duration;
931
+
932
+ // 处理视频接近结尾的情况
933
+ if (duration - clickTime < 1) {
934
+ clickTime = Math.min(clickTime, duration - 1.5);
935
+ }
936
+
937
+ // 记录用户点击的位置
938
+ userClickedPosition = clickTime;
939
+
940
+ console.log(`进度条触摸: ${percentage.toFixed(4)}, 时间: ${clickTime.toFixed(2)}/${duration.toFixed(2)}`);
941
+
942
+ e.stopPropagation();
943
+ dp.seek(clickTime);
944
+ }
945
+ </script>
946
+ </body>
947
+ </html>
privacy.html ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>隐私政策 - LibreTV</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="css/styles.css">
9
+ <!-- ...existing head code... -->
10
+ </head>
11
+ <body class="page-bg text-white">
12
+ <div class="container mx-auto px-4 py-8">
13
+ <header class="text-center mb-8">
14
+ <h1 class="text-5xl font-bold gradient-text">隐私政策</h1>
15
+ </header>
16
+ <main class="text-center">
17
+ <p class="text-gray-300 mb-4">
18
+ 我们尊重并保护您的隐私。LibreTV 不收集任何个人数据,且不会限制访问或使用本网站。
19
+ </p>
20
+ <p class="text-gray-300">
21
+ 本平台仅用于提供在线视频搜索与播放服务。所有数据均由第三方接口提供,我们不会存储或追踪用户信息。
22
+ </p>
23
+ </main>
24
+ <footer class="mt-12 text-center">
25
+ <a href="index.html" class="text-gray-400 hover:text-white transition-colors">回到首页</a>
26
+ </footer>
27
+ </div>
28
+ </body>
29
+ </html>
readme.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LibreTV - 免费在线视频搜索与观看平台
2
+
3
+ ## 📺 项目简介
4
+
5
+ LibreTV是一个轻量级、免费的在线视频搜索与观看平台,提供来自多个视频源的内容搜索与播放服务。无需注册,即开即用,支持多种设备访问。项目采用纯前端技术构建,可轻松部署在各类静态网站托管服务上。
6
+
7
+ 本项目基于 https://github.com/bestK/tv
8
+
9
+ 演示站:https://libretv.is-an.org/
10
+
11
+ <img src="https://testingcf.jsdelivr.net/gh/bestZwei/imgs@master/picgo/image-20250406231222216.png" alt="image-20250406231222216" style="zoom:67%;" />
12
+
13
+ **感谢 [NodeSupport](https://www.nodeseek.com/post-305185-1) 友情赞助**
14
+
15
+ ## ✨ 主要特性
16
+
17
+ - 🔍 多源视频搜索功能,覆盖电影、电视剧等内容
18
+ - 📱 响应式设计,完美支持电脑、平板和手机
19
+ - 🌐 聚合多个视频源,自动提取播放链接
20
+ - 🔄 支持自定义API接口,灵活扩展
21
+ - 💾 本地存储搜索历史,提升使用体验
22
+ - 🚀 纯静态部署,无需后端服务器
23
+ - 🛡️ 内置广告过滤功能,提供更干净的观影体验
24
+ - 🎬 自定义视频播放器,支持HLS流媒体格式
25
+ - ⌨️ 键盘快捷键支持,提高观影体验
26
+
27
+ ## ⌨️ 键盘快捷键
28
+
29
+ LibreTV播放器支持以下键盘快捷键:
30
+
31
+ - **Alt + 左箭头**:播放上一集
32
+ - **Alt + 右箭头**:播放下一集
33
+ - **空格键**:暂停/播放
34
+ - **左/右箭头**:快退/快进5秒
35
+ - **上/下箭头**:调整音量
36
+ - **F**:全屏/退出全屏
37
+
38
+ ### CMS采集站源兼容性
39
+
40
+ 本项目支持标准的苹果CMS V10 API格式。自定义API需遵循以下格式:
41
+ - 搜索接口: `https://example.com/api.php/provide/vod/?ac=videolist&wd=关键词`
42
+ - 详情接口: `https://example.com/api.php/provide/vod/?ac=detail&ids=视频ID`
43
+
44
+ **重要提示**: 像 `https://360zy.com/api.php/provide/vod` 这样的CMS源需要按照以下格式添加:
45
+ 1. 在设置面板中选择"自定义接口"
46
+ 2. 接口地址只填写到域名部分: `https://360zy.com`(不要包含`/api.php/provide/vod`部分)
47
+ 3. 项目会自动补全正确的路径格式
48
+
49
+ 如果CMS接口非标准格式,可能需要修改项目中的`config.js`文件中的`API_CONFIG.search.path`和`API_CONFIG.detail.path`配置。
50
+
51
+ ## 🛠️ 技术栈
52
+
53
+ - HTML5 + CSS3 + JavaScript (ES6+)
54
+ - Tailwind CSS (通过CDN引入)
55
+ - HLS.js 用于HLS流处理和广告过滤
56
+ - DPlayer 视频播放器核心
57
+ - 前端API请求拦截技术
58
+ - localStorage本地存储
59
+
60
+ ## 🚀 一键部署
61
+
62
+ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FbestZwei%2FLibreTV)
63
+
64
+ [![Deploy to Cloudflare Pages](https://img.shields.io/badge/Deploy%20to-Cloudflare%20Pages-blue?style=for-the-badge&logo=cloudflare)](https://dash.cloudflare.com/)
65
+
66
+ ## 🚀 部署指南
67
+
68
+ ### Cloudflare Pages部署
69
+
70
+ 1. Fork或克隆本仓库到你的GitHub账户
71
+ 2. 登录Cloudflare Dashboard,进入Pages服务
72
+ 3. 点击"创建项目",连接GitHub仓库
73
+ 4. 使用以下设置:
74
+ - 构建命令:留空(无需构建)
75
+ - 输出目录:留空(默认为根目录)
76
+ - 部署命令:留空
77
+ 5. 点击"保存并部署"
78
+
79
+ ### Vercel/Netlify部署
80
+
81
+ 类似Cloudflare Pages,只需连接仓库并部署即可,无需特殊配置。
82
+
83
+ ### 本地测试
84
+
85
+ 如果你想在本地测试,可以使用任何静态文件服务器:
86
+
87
+ ```bash
88
+ # 使用Python
89
+ python -m http.server 8080
90
+
91
+ # 或使用Node.js的http-server
92
+ npx http-server -p 8080
93
+ ```
94
+
95
+ ### Docker 部署
96
+
97
+ ```bash
98
+ docker pull bestzwei/libretv:latest
99
+ docker run -d --name libretv -p 8899:80 bestzwei/libretv:latest
100
+ ```
101
+
102
+ 访问 http://localhost:8899 查看效果。
103
+
104
+ ### Docker Compose 部署
105
+
106
+ 你也可以通过 Docker Compose 部署本项目。新建一个名为 `docker-compose.yaml` 的文件,内容如下:
107
+
108
+ ```yaml
109
+ version: '3'
110
+ services:
111
+ libretv:
112
+ image: bestzwei/libretv:latest
113
+ container_name: libretv
114
+ ports:
115
+ - "8899:80"
116
+ restart: unless-stopped
117
+ ```
118
+
119
+ ## 🔧 自定义配置
120
+
121
+ 项目主要配置在`js/config.js`文件中,你可以修改以下内容:
122
+
123
+ - `PROXY_URL`: 修改为你自己的代理服务地址
124
+ - `API_SITES`: 添加或修改视频源API接口
125
+ - `SITE_CONFIG`: 更改站点名称、描述等基本信息
126
+ - `PLAYER_CONFIG`: 调整播放器参数,如自动播放、广告过滤等
127
+ - `HIDE_BUILTIN_ADULT_APIS`: 用于控制是否隐藏内置的黄色采集站API,默认值为`true`。设置为`true`时,内置的某些敏感API将不会在设置面板中显示,可根据实际需要修改配置。
128
+
129
+ 注意:若使用docker部署,可进入容器,在`/usr/share/nginx/html/js`内修改相关配置
130
+
131
+ ## Star History
132
+
133
+ [![Star History Chart](https://api.star-history.com/svg?repos=bestZwei/LibreTV&type=Date)](https://www.star-history.com/#bestZwei/LibreTV&Date)
134
+
135
+ ## ⚠️ 免责声明
136
+
137
+ LibreTV 仅作为视频搜索工具,不存储、上传或分发任何视频内容。所有视频均来自第三方API接口提供的���索结果。如有侵权内容,请联系相应的内容提供方。
138
+
139
+ ## 🔄 更新日志
140
+
141
+ - 1.0.0 (2025-04-06): 初始版本发布
142
+ - 1.0.1 (2025-04-07): 添加广告过滤功能,优化播放器性能
143
+ - 1.0.2 (2025-04-08): 分离了播放页面,优化视频源API兼容性
144
+ - 1.0.3 (2025-04-13): 性能优化、ui优化、更新设置功能
robots.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /js/
4
+ Disallow: /css/
5
+
6
+ Sitemap: https://libretv.is-an.org/sitemap.xml
sitemap.xml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url>
4
+ <loc>https://libretv.is-an.org/</loc>
5
+ <lastmod>2025-04-06</lastmod>
6
+ <changefreq>weekly</changefreq>
7
+ <priority>1.0</priority>
8
+ </url>
9
+ <url>
10
+ <loc>https://libretv.is-an.org/about.html</loc>
11
+ <lastmod>2025-04-06</lastmod>
12
+ <changefreq>monthly</changefreq>
13
+ <priority>0.8</priority>
14
+ </url>
15
+ <url>
16
+ <loc>https://libretv.is-an.org/privacy.html</loc>
17
+ <lastmod>2025-04-06</lastmod>
18
+ <changefreq>monthly</changefreq>
19
+ <priority>0.5</priority>
20
+ </url>
21
+ </urlset>
watch.html ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="refresh" content="0; url=player.html">
6
+ <title>重定向到播放器</title>
7
+ </head>
8
+ <body>
9
+ <p>如果您没有被自动重定向,请<a href="player.html">点击这里</a>前往播放页面。</p>
10
+ </body>
11
+ </html>