Upload 18 files
Browse files- .dockerignore +2 -0
- .github/workflows/docker-build.yml +43 -0
- .github/workflows/sync.yml +39 -0
- Dockerfile +12 -0
- LICENSE +201 -0
- about.html +30 -0
- css/styles.css +600 -0
- index.html +262 -19
- js/api.js +607 -0
- js/app.js +933 -0
- js/config.js +195 -0
- js/ui.js +234 -0
- player.html +947 -0
- privacy.html +29 -0
- readme.md +144 -0
- robots.txt +6 -0
- sitemap.xml +21 -0
- 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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="关闭设置">×</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">×</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, '<')
|
| 693 |
+
.replace(/>/g, '>')
|
| 694 |
+
.replace(/"/g, '"');
|
| 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, '"')}"` : '';
|
| 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, '<') ?
|
| 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, '<')}
|
| 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, '<')}
|
| 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, '"')
|
| 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, '"')}', ${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, '<').replace(/>/g, '>');
|
| 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 |
+
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FbestZwei%2FLibreTV)
|
| 63 |
+
|
| 64 |
+
[](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 |
+
[](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>
|