chore: tidy plugin root and bump version to 0.7.23
This commit is contained in:
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
description: anime_downloader 플러그인 코딩 규칙
|
|
||||||
---
|
|
||||||
|
|
||||||
# 코딩 규칙
|
|
||||||
|
|
||||||
## 타입 힌트
|
|
||||||
- 모든 새 코드에 타입 힌트 필수 적용
|
|
||||||
- 함수 파라미터와 반환값에 타입 지정
|
|
||||||
- 클래스 변수에도 타입 지정
|
|
||||||
|
|
||||||
## 코드 스타일
|
|
||||||
- 한국어 주석 사용
|
|
||||||
- 로거 메시지는 영어/한국어 혼용 가능
|
|
||||||
- 에러 메시지는 간결하게
|
|
||||||
|
|
||||||
## 커맨드 실행 규칙
|
|
||||||
- `rm`, `mv`, `cp` 등 파일을 변조하거나 삭제할 수 있는 파괴적인 명령은 반드시 사용자 승인 필요 (`SafeToAutoRun: false`)
|
|
||||||
- `cat`, `grep`, `sed`, `ps`, `lsof`, `curl` (단순 조회용) 등 부수 효과가 없는 조회성 명령은 자동 실행 허용 (`SafeToAutoRun: true`)
|
|
||||||
|
|
||||||
## FlaskFarm 관련
|
|
||||||
- flaskfarm 코어 소스 수정 최소화 (외부 프로젝트)
|
|
||||||
- 플러그인 내에서 해결 가능한 것은 플러그인에서 처리
|
|
||||||
@@ -134,6 +134,8 @@ dmypy.json
|
|||||||
config.yaml
|
config.yaml
|
||||||
lib2/
|
lib2/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.agents/
|
||||||
|
.agent/
|
||||||
memo.txt
|
memo.txt
|
||||||
*.zip
|
*.zip
|
||||||
flaskfarm.sh
|
flaskfarm.sh
|
||||||
@@ -151,6 +153,8 @@ lib/support/site/wavve.py
|
|||||||
lib/support/site/seezn.py
|
lib/support/site/seezn.py
|
||||||
lib/support/site/kakaotv.py
|
lib/support/site/kakaotv.py
|
||||||
*.bat
|
*.bat
|
||||||
|
docs/
|
||||||
|
error_logs/
|
||||||
output/
|
output/
|
||||||
*.mkv
|
*.mkv
|
||||||
|
|
||||||
@@ -164,3 +168,4 @@ bin/
|
|||||||
debug_*.py
|
debug_*.py
|
||||||
check_top_api.py
|
check_top_api.py
|
||||||
d_ohli24.py
|
d_ohli24.py
|
||||||
|
dev_scratch/
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# 2026-03-23 Work History
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
- Strengthened Ohli24 browse-cache validation so list/search cache only survives when at least one row contains both a link and a title.
|
|
||||||
- Hardened Ohli24 list, auto-list, and search parsing against nested `post-title` markup and missing image attributes.
|
|
||||||
- Prevented malformed rows from crashing the entire list response by skipping incomplete entries with warning logs.
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
- Added `_extract_text()` and `_extract_first()` helpers in [`mod_ohli24.py`](/Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/mod_ohli24.py) to avoid repeated unsafe XPath `[0]` access.
|
|
||||||
- Extended `_is_valid_cached_html()` to parse list/search rows and reject cache payloads that only contain placeholder rows.
|
|
||||||
- Updated changelog and plugin version to `0.7.20`.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
- `python3 -m py_compile /Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/mod_ohli24.py`
|
|
||||||
- Smoke-checked validator behavior for valid list/detail HTML and blocked HTML patterns during implementation.
|
|
||||||
@@ -84,6 +84,12 @@
|
|||||||
|
|
||||||
## 📝 변경 이력 (Changelog)
|
## 📝 변경 이력 (Changelog)
|
||||||
|
|
||||||
|
### v0.7.23 (2026-04-01)
|
||||||
|
- **루트 정리 및 개발 보관 폴더 분리**:
|
||||||
|
- 루트에 흩어져 있던 `test_*`, 디버그 스크립트, 임시 HTML/노트북/구버전 백업 파일을 `dev_scratch/` 폴더로 이동해 작업용 산출물을 한곳에 모았습니다.
|
||||||
|
- `dev_scratch/` 는 `.gitignore` 에 추가하여 로컬 검증 파일이 저장소 루트를 다시 어지르지 않도록 정리했습니다.
|
||||||
|
- 루트에 있던 `2026-03-23.history.md` 도 `docs/history/` 로 이동해 작업 이력 위치를 일관되게 맞췄습니다.
|
||||||
|
|
||||||
### v0.7.20 (2026-03-23)
|
### v0.7.20 (2026-03-23)
|
||||||
- **Ohli24 캐시/목록 파싱 보강**:
|
- **Ohli24 캐시/목록 파싱 보강**:
|
||||||
- 목록/검색 캐시 저장 전에 실제로 파싱 가능한 `href + title` 행이 있는지 검사하여 skeleton/불완전 HTML이 캐시에 남지 않도록 했습니다.
|
- 목록/검색 캐시 저장 전에 실제로 파싱 가능한 `href + title` 행이 있는지 검사하여 skeleton/불완전 HTML이 캐시에 남지 않도록 했습니다.
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# 2026-03-26 작업 이력
|
|
||||||
|
|
||||||
## ohli24 큐 탭 초기화 버튼 오류 수정
|
|
||||||
|
|
||||||
- `ohli24` 큐 탭의 `초기화` 버튼 클릭 시 확인 모달 뒤에 `{ret: 'error'}`가 떨어지고 실제 초기화가 진행되지 않는 문제를 점검했다.
|
|
||||||
- 원인은 [mod_ohli24.py](/Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/mod_ohli24.py) 의 `queue_command` 처리에서 `req.form["entity_id"]`를 무조건 읽는 방식이었다.
|
|
||||||
- 큐 템플릿은 `reset`, `delete_completed` 요청 시 `entity_id` 없이 `globalSendCommand('reset', null, ...)` 형태로 보내므로, 서버가 `reset` 분기까지 내려가기 전에 실패할 수 있었다.
|
|
||||||
- `queue_command`를 [mod_anilife.py](/Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/mod_anilife.py) 와 같은 안전 패턴으로 정리해 `command`, `entity_id`를 `req.form.get(...)`로 읽도록 수정했다.
|
|
||||||
- `reset` 처리 시 GDM 다운로드 취소만 하지 않고, [ffmpeg_queue_v1.py](/Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/lib/ffmpeg_queue_v1.py) 의 `reset` 경로를 통해 `self.queue` 런타임 큐와 `entity_list`도 함께 비우도록 보강했다.
|
|
||||||
- 일반 큐 명령 fallback도 `entity_id`가 비어 있을 수 있는 경우를 고려해 `int(entity_id) if digit else 0` 패턴으로 안전화했다.
|
|
||||||
|
|
||||||
## 2026-03-26 검증
|
|
||||||
|
|
||||||
- `python -m py_compile /Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/mod_ohli24.py`: success
|
|
||||||
- 수정 후 [mod_ohli24.py](/Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/mod_ohli24.py) 의 `queue_command` 분기에서 `reset` 요청이 `entity_id` 없이도 처리 가능한지 코드 경로를 재확인했다.
|
|
||||||
|
|
||||||
## ohli24 큐 진행률 실시간 반영 보강
|
|
||||||
|
|
||||||
- [anime_downloader_ohli24_queue.html](/Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader/templates/anime_downloader_ohli24_queue.html) 에서 큐 페이지 Socket.IO 네임스페이스가 `ohli24`가 아닌 `linkkf`로 하드코딩되어 있어, 모듈 전용 큐 이벤트를 정상 수신하지 못하는 문제를 확인했다.
|
|
||||||
- 같은 템플릿이 GDM 이벤트를 `status`로 수신하고 있었는데, 실제 [gommi_downloader_manager/mod_queue.py](/Volumes/WD/Users/Work/python/ff_dev_plugins/gommi_downloader_manager/mod_queue.py) 는 `/gommi_downloader_manager` 네임스페이스에 `download_status` 이벤트를 emit하고 있었다.
|
|
||||||
- 추가로 `autoRefreshList()`가 항목 개수 변화가 있을 때만 `renderList()`를 호출하고 있어, 진행률/속도만 변하는 구간에서는 폴링 fallback이 있어도 화면이 갱신되지 않았다.
|
|
||||||
- 템플릿을 `PACKAGE_NAME`, `MODULE_NAME` 기반 동적 네임스페이스로 바꾸고, framework 이벤트명도 `MODULE_NAME + '_status'`를 쓰도록 정리했다.
|
|
||||||
- GDM 소켓은 `download_status`를 수신하도록 맞췄고, 이미 렌더된 항목은 전체 새로고침 대신 progress/status/button 영역만 즉시 갱신하게 보강했다.
|
|
||||||
- 소켓 이벤트를 놓치더라도 현재 DOM과 서버 목록을 비교해 진행률 차이가 있으면 조용히 다시 렌더하도록 fallback을 추가했다.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
title: "애니 다운로더"
|
title: "애니 다운로더"
|
||||||
version: 0.7.22
|
version: 0.7.23
|
||||||
package_name: "anime_downloader"
|
package_name: "anime_downloader"
|
||||||
developer: "projectdx"
|
developer: "projectdx"
|
||||||
description: "anime downloader"
|
description: "anime downloader"
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import zendriver as zd
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
async def test():
|
|
||||||
try:
|
|
||||||
browser = await zd.start(headless=True)
|
|
||||||
page = await browser.get("about:blank")
|
|
||||||
|
|
||||||
# Test header setting
|
|
||||||
headers = {"Referer": "https://v2.linkkf.app/"}
|
|
||||||
try:
|
|
||||||
await page.send(zd.cdp.network.enable())
|
|
||||||
headers_obj = zd.cdp.network.Headers(headers)
|
|
||||||
await page.send(zd.cdp.network.set_extra_http_headers(headers_obj))
|
|
||||||
print("Successfully set headers")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to set headers: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
methods = [m for m in dir(page) if not m.startswith("_")]
|
|
||||||
print(json.dumps({"methods": methods}))
|
|
||||||
await browser.stop()
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
print(json.dumps({"error": str(e), "traceback": traceback.format_exc()}))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(test())
|
|
||||||
@@ -23,21 +23,6 @@ from lxml import html
|
|||||||
from urllib import parse
|
from urllib import parse
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
packages = [
|
|
||||||
"beautifulsoup4",
|
|
||||||
"requests-cache",
|
|
||||||
"cloudscraper",
|
|
||||||
"selenium_stealth",
|
|
||||||
"webdriver_manager",
|
|
||||||
]
|
|
||||||
for package in packages:
|
|
||||||
try:
|
|
||||||
import package
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# main(["install", package])
|
|
||||||
os.system(f"pip install {package}")
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import cloudscraper
|
import cloudscraper
|
||||||
|
|
||||||
|
|||||||
-124
@@ -1,124 +0,0 @@
|
|||||||
# anime_downloader 플러그인 구조 분석
|
|
||||||
|
|
||||||
## 📌 개요
|
|
||||||
|
|
||||||
FlaskFarm용 **애니메이션 다운로드 플러그인**으로, 여러 애니메이션 스트리밍 사이트에서 콘텐츠를 검색하고 다운로드할 수 있는 기능을 제공합니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ 전체 구조
|
|
||||||
|
|
||||||
```
|
|
||||||
anime_downloader/
|
|
||||||
├── __init__.py # 플러그인 초기화 (현재 비활성화됨)
|
|
||||||
├── setup.py # 플러그인 설정 및 메뉴 구조, 모듈 로드
|
|
||||||
├── info.yaml # 플러그인 메타데이터 (이름, 버전, 개발자)
|
|
||||||
├── mod_ohli24.py # 애니24 사이트 모듈 (1,542줄)
|
|
||||||
├── mod_anilife.py # 애니라이프 사이트 모듈 (1,322줄)
|
|
||||||
├── mod_linkkf.py # 링크애니 사이트 모듈 (1,449줄)
|
|
||||||
├── lib/ # 공용 라이브러리
|
|
||||||
│ ├── crawler.py # 웹 크롤링 엔진 (Playwright, Selenium, Cloudscraper)
|
|
||||||
│ ├── ffmpeg_queue_v1.py# FFmpeg 다운로드 큐 관리
|
|
||||||
│ ├── util.py # 유틸리티 함수 (파일명 정리, 타이밍 등)
|
|
||||||
│ └── misc.py # 비동기 실행 헬퍼 함수
|
|
||||||
├── templates/ # HTML 템플릿 (18개 파일)
|
|
||||||
├── static/ # CSS, JS, 이미지 리소스
|
|
||||||
├── bin/ # 플랫폼별 바이너리 (Darwin, Linux)
|
|
||||||
├── nest_api/ # 애니 API 관련 (서브디렉토리)
|
|
||||||
└── yommi_api/ # 커스텀 API 관련
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 핵심 컴포넌트
|
|
||||||
|
|
||||||
### 1. setup.py - 플러그인 엔트리포인트
|
|
||||||
|
|
||||||
| 항목 | 설명 |
|
|
||||||
|------|------|
|
|
||||||
| `__menu` | 3개 사이트별 서브메뉴 (설정, 요청, 큐, 검색, 목록) |
|
|
||||||
| `setting` | DB 사용, 기본 설정, 홈 모듈(`ohli24`) 지정 |
|
|
||||||
| `P` | FlaskFarm 플러그인 인스턴스 생성 |
|
|
||||||
| 모듈 로드 | `LogicOhli24`, `LogicAniLife`, `LogicLinkkf` |
|
|
||||||
|
|
||||||
### 2. 사이트 모듈 (mod_*.py)
|
|
||||||
|
|
||||||
각 모듈은 동일한 구조를 따릅니다:
|
|
||||||
|
|
||||||
| 클래스 | 역할 |
|
|
||||||
|--------|------|
|
|
||||||
| `LogicXxx` | 사이트별 비즈니스 로직 (검색, 시리즈 정보, 다운로드 추가) |
|
|
||||||
| `XxxQueueEntity` | 다운로드 큐 항목 (에피소드 정보, 상태 관리) |
|
|
||||||
| `ModelXxxItem` | SQLAlchemy DB 모델 (다운로드 기록 저장) |
|
|
||||||
|
|
||||||
**LogicXxx 주요 메서드:**
|
|
||||||
|
|
||||||
- `process_menu()` / `process_ajax()` - 웹 요청 처리
|
|
||||||
- `get_series_info()` - 시리즈/에피소드 정보 파싱
|
|
||||||
- `get_anime_info()` / `get_search_result()` - 목록/검색
|
|
||||||
- `add()` - 다운로드 큐에 추가
|
|
||||||
- `scheduler_function()` - 자동 다운로드 스케줄러
|
|
||||||
- `plugin_load()` / `plugin_unload()` - 생명주기 관리
|
|
||||||
|
|
||||||
### 3. lib/crawler.py - 웹 크롤링 엔진
|
|
||||||
|
|
||||||
| 메서드 | 기술 |
|
|
||||||
|--------|------|
|
|
||||||
| `get_html_requests()` | 기본 requests 요청 |
|
|
||||||
| `get_html_playwright()` | Playwright 비동기 (헤드리스 브라우저) |
|
|
||||||
| `get_html_playwright_sync()` | Playwright 동기 |
|
|
||||||
| `get_html_selenium()` | Selenium WebDriver |
|
|
||||||
| `get_html_cloudflare()` | Cloudscraper (CF 우회) |
|
|
||||||
|
|
||||||
### 4. lib/ffmpeg_queue_v1.py - 다운로드 큐
|
|
||||||
|
|
||||||
| 클래스 | 역할 |
|
|
||||||
|--------|------|
|
|
||||||
| `FfmpegQueueEntity` | 개별 다운로드 항목 (URL, 파일경로, 상태) |
|
|
||||||
| `FfmpegQueue` | 큐 관리자 (스레드 기반 다운로드, 동시 다운로드 수 제어) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🖥️ 지원 사이트 (3개)
|
|
||||||
|
|
||||||
| 모듈 | 사이트 | URI |
|
|
||||||
|------|--------|-----|
|
|
||||||
| `mod_ohli24.py` | 애니24 (ohli24) | `/ohli24` |
|
|
||||||
| `mod_anilife.py` | 애니라이프 | `/anilife` |
|
|
||||||
| `mod_linkkf.py` | 링크애니 | `/linkkf` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 템플릿 구조
|
|
||||||
|
|
||||||
각 사이트별로 6개 템플릿 제공:
|
|
||||||
|
|
||||||
- `*_setting.html` - 사이트 설정
|
|
||||||
- `*_request.html` - 다운로드 요청 페이지
|
|
||||||
- `*_queue.html` - 다운로드 큐 현황
|
|
||||||
- `*_search.html` - 검색 인터페이스
|
|
||||||
- `*_list.html` - 다운로드 목록
|
|
||||||
- `*_category.html` - 카테고리 탐색
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 동작 흐름
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
A[사용자] --> B[검색/탐색]
|
|
||||||
B --> C[시리즈 선택]
|
|
||||||
C --> D[에피소드 선택]
|
|
||||||
D --> E[다운로드 큐 추가]
|
|
||||||
E --> F[FfmpegQueue]
|
|
||||||
F --> G[FFmpeg 다운로드]
|
|
||||||
G --> H[DB 기록 저장]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 주의사항
|
|
||||||
|
|
||||||
1. **개발 모드**: `setup.py`에서 `DEFINE_DEV = True`로 설정되어 직접 모듈 import
|
|
||||||
2. **`__init__.py` 비활성화**: 현재 주석 처리되어 `setup.py`가 실제 엔트리포인트 역할
|
|
||||||
3. **크롤링 기술 혼용**: Cloudflare 우회를 위해 Playwright, Selenium, cloudscraper 등 다양한 기술 사용
|
|
||||||
-102
@@ -1,102 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# @Time : 2022/02/08 2:57 PM
|
|
||||||
# @Author : yommi
|
|
||||||
# @Site :
|
|
||||||
# @File : plugin
|
|
||||||
# @Software: PyCharm
|
|
||||||
|
|
||||||
import os, traceback
|
|
||||||
|
|
||||||
# third-party
|
|
||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
# sjva 공용
|
|
||||||
from framework.logger import get_logger
|
|
||||||
from framework import app, path_data
|
|
||||||
from plugin import get_model_setting, Logic, default_route, PluginUtil
|
|
||||||
|
|
||||||
|
|
||||||
#######################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class P(object):
|
|
||||||
package_name = __name__.split(".")[0]
|
|
||||||
logger = get_logger(package_name)
|
|
||||||
blueprint = Blueprint(
|
|
||||||
package_name,
|
|
||||||
package_name,
|
|
||||||
url_prefix="/%s" % package_name,
|
|
||||||
template_folder=os.path.join(os.path.dirname(__file__), "templates"),
|
|
||||||
static_folder="static",
|
|
||||||
)
|
|
||||||
menu = {
|
|
||||||
"main": [package_name, "애니 다운로드"],
|
|
||||||
"sub": [
|
|
||||||
["ohli24", "OHLI24"],
|
|
||||||
["linkkf", "LINKKF"],
|
|
||||||
["anilife", "애니라이프"],
|
|
||||||
["log", "로그"],
|
|
||||||
],
|
|
||||||
"category": "vod",
|
|
||||||
"sub2": {
|
|
||||||
"ohli24": [
|
|
||||||
["setting", "설정"],
|
|
||||||
["request", "요청"],
|
|
||||||
["queue", "큐"],
|
|
||||||
["category", "검색"],
|
|
||||||
["list", "목록"],
|
|
||||||
],
|
|
||||||
"linkkf": [
|
|
||||||
["setting", "설정"],
|
|
||||||
["request", "요청"],
|
|
||||||
["queue", "큐"],
|
|
||||||
["category", "검색"],
|
|
||||||
["list", "목록"],
|
|
||||||
],
|
|
||||||
"anilife": [
|
|
||||||
["setting", "설정"],
|
|
||||||
["request", "요청"],
|
|
||||||
["category", "검색"],
|
|
||||||
["queue", "큐"],
|
|
||||||
["list", "목록"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
plugin_info = {
|
|
||||||
"version": "0.1.2.0",
|
|
||||||
"name": "anime_downloader",
|
|
||||||
"category_name": "vod",
|
|
||||||
"icon": "",
|
|
||||||
"developer": "soju6jan && projectdx",
|
|
||||||
"description": "비디오 다운로드",
|
|
||||||
"home": "http://yommi.duckdns.org:20080/projectdx/anime-downloader",
|
|
||||||
"more": "",
|
|
||||||
}
|
|
||||||
ModelSetting = get_model_setting(package_name, logger)
|
|
||||||
logic = None
|
|
||||||
module_list = None
|
|
||||||
home_module = "ohli24"
|
|
||||||
|
|
||||||
|
|
||||||
# 초기화 함수
|
|
||||||
def initialize():
|
|
||||||
try:
|
|
||||||
app.config["SQLALCHEMY_BINDS"][P.package_name] = "sqlite:///%s" % (
|
|
||||||
os.path.join(
|
|
||||||
path_data, "db", "{package_name}.db".format(package_name=P.package_name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
PluginUtil.make_info_json(P.plugin_info, __file__)
|
|
||||||
from .logic_ohli24 import LogicOhli24
|
|
||||||
from .logic_anilife import LogicAniLife
|
|
||||||
|
|
||||||
# P.module_list = [LogicOhli24(P), LogicLinkkf(P)]
|
|
||||||
P.module_list = [LogicOhli24(P), LogicAniLife(P)]
|
|
||||||
P.logic = Logic(P)
|
|
||||||
default_route(P)
|
|
||||||
except Exception as e:
|
|
||||||
P.logger.error("Exception:%s", e)
|
|
||||||
P.logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
initialize()
|
|
||||||
@@ -1,810 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="kr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
||||||
<meta name='robots' content='noindex, follow, max-image-preview:large' />
|
|
||||||
<script type='text/javascript' id='mv_jquery-js-extra'>
|
|
||||||
/* <![CDATA[ */
|
|
||||||
var mv_base = {"ajaxurl":"https:\/\/linkkf.live\/wp-admin\/admin-ajax.php"};
|
|
||||||
/* ]]> */
|
|
||||||
</script>
|
|
||||||
<script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js' id='mv_jquery-js'></script>
|
|
||||||
<script type='text/javascript' src='https://linkkf.live/wp-content/themes/kfbeta16/assets/js/layer/layer.js' id='mv_layui-js'></script>
|
|
||||||
<script type='text/javascript' src='https://linkkf.live/wp-content/themes/kfbeta16/assets/js/mytheme-ui.js' id='mv_mytheme_ui-js'></script>
|
|
||||||
<script type='text/javascript' src='https://linkkf.live/wp-content/themes/kfbeta16/assets/js/mytheme-cms.js' id='mv_mytheme_cms-js'></script>
|
|
||||||
<script type='text/javascript'>
|
|
||||||
/* <![CDATA[ */
|
|
||||||
var taqyeem = {"ajaxurl":"https://linkkf.live/wp-admin/admin-ajax.php" , "your_rating":"Your Rating:"};
|
|
||||||
/* ]]> */
|
|
||||||
</script>
|
|
||||||
<link rel="icon" href="https://linkkf.live/wp-content/uploads/2020/09/cropped-logo-32x32.png" sizes="32x32" />
|
|
||||||
<link rel="icon" href="https://linkkf.live/wp-content/uploads/2020/09/cropped-logo-192x192.png" sizes="192x192" />
|
|
||||||
<link rel="apple-touch-icon" href="https://linkkf.live/wp-content/uploads/2020/09/cropped-logo-180x180.png" />
|
|
||||||
<meta name="msapplication-TileImage" content="https://linkkf.live/wp-content/uploads/2020/09/cropped-logo-270x270.png" />
|
|
||||||
<link href="https://linkkf.live/wp-content/themes/kfbeta16/style.css" rel="stylesheet" type="text/css" />
|
|
||||||
<link rel="author" href="https://1.bp.blogspot.com/-yjRTBw8mtN4/X2f2XSQ8o5I/AAAAAAAAKn8/oCxwBIPZo5AEgaR-Ak7tVvPJiHEHKTjpQCLcBGAsYHQ/s52/logo.png" />
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Google tag (gtag.js) -->
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-YKQT0BN847"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
|
|
||||||
gtag('config', 'G-YKQT0BN847');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
#masthead {
|
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
|
||||||
padding: 0 16px; height: 56px; background-color: #202020;
|
|
||||||
top: 0; z-index: 100; border-bottom: 1px solid #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left { display: flex; align-items: center; gap: 15px; min-width: 150px; z-index: 2; }
|
|
||||||
.logo-text { font-size: 18px; font-weight: bold; color: #fff; letter-spacing: -0.5px; display: flex; align-items: center; gap: 5px; }
|
|
||||||
|
|
||||||
.header-center { flex: 0 1 700px; display: flex; justify-content: center; margin: 0 20px; }
|
|
||||||
|
|
||||||
.yt-search-form { display: flex; width: 100%; max-width: 600px; position: relative; }
|
|
||||||
|
|
||||||
.yt-search-input {
|
|
||||||
width: 100%; padding: 0 15px; height: 40px; background-color: #121212;
|
|
||||||
border: 1px solid #303030; border-right: none; border-radius: 40px 0 0 40px;
|
|
||||||
color: #fff; font-size: 16px; outline: none; box-shadow: inset 0 1px 2px #000000;
|
|
||||||
}
|
|
||||||
.yt-search-input:focus { border-color: #1c62b9; }
|
|
||||||
|
|
||||||
.yt-search-btn {
|
|
||||||
width: 64px; height: 40px; background-color: #222; border: 1px solid #303030;
|
|
||||||
border-radius: 0 40px 40px 0; display: flex; align-items: center; justify-content: center;
|
|
||||||
}
|
|
||||||
.yt-search-btn:hover { background-color: #303030; }
|
|
||||||
.yt-search-btn svg { fill: #fff; width: 24px; height: 24px; }
|
|
||||||
|
|
||||||
.mobile-back-btn {
|
|
||||||
display: none; background: transparent; border: none; padding: 10px; margin-right: 5px;
|
|
||||||
}
|
|
||||||
.mobile-back-btn svg { fill: #fff; width: 24px; height: 24px; }
|
|
||||||
|
|
||||||
.header-right { display: flex; align-items: center; gap: 10px; min-width: auto; justify-content: flex-end; z-index: 2; }
|
|
||||||
|
|
||||||
.mobile-search-trigger {
|
|
||||||
display: none; background: transparent; border: none; padding: 8px; border-radius: 50%;
|
|
||||||
}
|
|
||||||
.mobile-search-trigger:active { background-color: #2a2a2a; }
|
|
||||||
.mobile-search-trigger svg { fill: #fff; width: 24px; height: 24px; }
|
|
||||||
|
|
||||||
.auth-btn-link {
|
|
||||||
display: flex; align-items: center; gap: 6px;
|
|
||||||
padding: 6px 12px; border: 1px solid #303030; border-radius: 18px;
|
|
||||||
color: #3ea6ff; font-size: 14px; font-weight: 500; background: rgba(38, 56, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar-wrap {
|
|
||||||
width: 32px; height: 32px; border-radius: 50%; overflow: hidden;
|
|
||||||
cursor: pointer; background: #333; border: 1px solid #444;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.user-avatar-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
||||||
|
|
||||||
.header-avatar-letter {
|
|
||||||
width: 100%; height: 100%; display: flex;
|
|
||||||
justify-content: center; align-items: center;
|
|
||||||
color: #fff; font-weight: bold; text-transform: uppercase;
|
|
||||||
font-size: 14px; user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout-mini {
|
|
||||||
font-size: 12px; color: #aaa; background: transparent; border: none; cursor: pointer; padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sub-nav {
|
|
||||||
display: flex; justify-content: center; gap: 10px; padding: 10px 15px;
|
|
||||||
background-color: #202020; border-bottom: 1px solid #2a2a2a;
|
|
||||||
overflow-x: auto; white-space: nowrap; top: 56px; z-index: 99;
|
|
||||||
}
|
|
||||||
#sub-nav::-webkit-scrollbar { display: none; }
|
|
||||||
.nav-chip {
|
|
||||||
display: flex; align-items: center; gap: 6px; padding: 6px 14px;
|
|
||||||
background-color: #222; border: 1px solid #333; color: #fff;
|
|
||||||
border-radius: 8px; font-size: 14px; font-weight: 500;
|
|
||||||
}
|
|
||||||
.nav-chip.active { background-color: #f1f1f1; color: #0f0f0f; }
|
|
||||||
.nav-chip svg { width: 16px; height: 16px; fill: currentColor; }
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header-center { display: none; }
|
|
||||||
.mobile-search-trigger { display: block; }
|
|
||||||
.header-right { min-width: auto; }
|
|
||||||
|
|
||||||
body.search-active .header-left { display: none; }
|
|
||||||
body.search-active .header-right { display: none; }
|
|
||||||
|
|
||||||
body.search-active .header-center {
|
|
||||||
display: flex; flex: 1; margin: 0; padding: 0 5px;
|
|
||||||
animation: fadeIn 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.search-active .mobile-back-btn { display: block; }
|
|
||||||
body.search-active .yt-search-input { border-radius: 20px 0 0 20px; }
|
|
||||||
body.search-active .yt-search-btn { border-radius: 0 20px 20px 0; background: #2a2a2a; }
|
|
||||||
|
|
||||||
#sub-nav { justify-content: flex-start; padding-left: 10px; }
|
|
||||||
.auth-btn-link span { display: none; }
|
|
||||||
}
|
|
||||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
||||||
|
|
||||||
.auth-modal { display: none; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); backdrop-filter: blur(3px); }
|
|
||||||
.auth-content { background-color: #1e1e1e; margin: 10% auto; padding: 30px; border: 1px solid #444; width: 350px; border-radius: 10px; position: relative; color: #eee; }
|
|
||||||
.close-auth { position: absolute; top: 10px; right: 15px; cursor: pointer; font-size: 24px; }
|
|
||||||
.auth-form input { width: 100%; padding: 12px; margin: 10px 0; background: #2a2a2a; border: 1px solid #444; color: #fff; border-radius: 4px; box-sizing: border-box;}
|
|
||||||
.submit-btn { width: 100%; background: #0073aa; color: #fff; padding: 12px; border: none; border-radius: 4px; font-weight: bold; margin-top: 10px; }
|
|
||||||
.auth-switch { text-align: center; margin-top: 15px; font-size: 0.9em; color: #aaa; }
|
|
||||||
.auth-msg { text-align: center; margin-top: 10px; min-height: 20px; font-size: 0.9em; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<header id="masthead">
|
|
||||||
<div class="header-left">
|
|
||||||
<a href="/" class="logo-text"><strong>Linkkf</strong></a>
|
|
||||||
<div style="display:flex; gap:8px; margin-left:10px; opacity:0.7;">
|
|
||||||
<a href="https://www.youtube.com/@linkkfanime" target="_blank"><img src="https://cdn.jsdelivr.net/gh/756751uosmaqy/wpp@main/yt.png" width="18" height="18"></a>
|
|
||||||
<a href="https://x.com/linkkfcom" target="_blank"><img src="https://cdn.jsdelivr.net/gh/756751uosmaqy/wpp@main/tw.png" width="18" height="18"></a>
|
|
||||||
<a href="https://www.tiktok.com/@linkkfani" target="_blank"><img src="https://cdn.jsdelivr.net/gh/756751uosmaqy/wpp@main/tt.png" width="18" height="18"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-center">
|
|
||||||
<form role="search" method="get" class="yt-search-form" action="https://linkkf.live/">
|
|
||||||
<button type="button" class="mobile-back-btn" onclick="toggleMobileSearch(false)">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"></path></svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input type="text" class="yt-search-input" placeholder="검색 ..." value="원펀맨" name="s" id="masthead-search-term">
|
|
||||||
|
|
||||||
<button type="submit" class="yt-search-btn">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M20.87,20.17l-5.59-5.59C16.35,13.35,17,11.75,17,10c0-3.87-3.13-7-7-7s-7,3.13-7,7s3.13,7,7,7c1.75,0,3.35-0.65,4.58-1.71 l5.59,5.59L20.87,20.17z M10,16c-3.31,0-6-2.69-6-6s2.69-6,6-6s6,2.69,6,6S13.31,16,10,16z"></path></svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-right">
|
|
||||||
<button class="mobile-search-trigger" onclick="toggleMobileSearch(true)">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M20.87,20.17l-5.59-5.59C16.35,13.35,17,11.75,17,10c0-3.87-3.13-7-7-7s-7,3.13-7,7s3.13,7,7,7c1.75,0,3.35-0.65,4.58-1.71 l5.59,5.59L20.87,20.17z M10,16c-3.31,0-6-2.69-6-6s2.69-6,6-6s6,2.69,6,6S13.31,16,10,16z"></path></svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="guest-buttons" style="display:flex;">
|
|
||||||
<a href="/login/" class="auth-btn-link">
|
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
|
|
||||||
<span>로그인</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="user-info" style="display:none; align-items:center; gap:10px;">
|
|
||||||
<a href="/login/" class="user-avatar-wrap" id="header-avatar-link" title="user-info">
|
|
||||||
<img id="header-user-avatar" src="" alt="Avatar">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<nav id="sub-nav">
|
|
||||||
<a href="/" class="nav-chip ">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg> 홈
|
|
||||||
</a>
|
|
||||||
<a href="/rr/" class="nav-chip">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg> 리뷰
|
|
||||||
</a>
|
|
||||||
<a href="/gg" class="nav-chip">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/></svg> 추천
|
|
||||||
</a>
|
|
||||||
<a href="/favorites/" class="nav-chip ">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> 찜 목록
|
|
||||||
</a>
|
|
||||||
<a href="/history/" class="nav-chip ">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg> 시청 기록
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="auth-modal" class="auth-modal">
|
|
||||||
<div class="auth-content">
|
|
||||||
<span class="close-auth" onclick="closeAuthModal()">×</span>
|
|
||||||
|
|
||||||
<div id="form-login" class="auth-form">
|
|
||||||
<h2>로그인</h2>
|
|
||||||
<input type="email" id="login-email" placeholder="이메일">
|
|
||||||
<input type="password" id="login-pass" placeholder="비밀번호">
|
|
||||||
<button onclick="handleLogin()" class="submit-btn">로그인</button>
|
|
||||||
<p class="auth-switch">계정이 없으신가요? <a href="#" onclick="switchAuth('register')">회원가입</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="form-register" class="auth-form" style="display:none;">
|
|
||||||
<h2>회원가입</h2>
|
|
||||||
<input type="text" id="reg-name" placeholder="닉네임">
|
|
||||||
<input type="email" id="reg-email" placeholder="이메일">
|
|
||||||
<input type="password" id="reg-pass" placeholder="비밀번호">
|
|
||||||
<button onclick="handleRegister()" class="submit-btn">회원가입</button>
|
|
||||||
<p class="auth-switch">이미 계정이 있으신가요? <a href="#" onclick="switchAuth('login')">로그인</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p id="auth-msg" class="auth-msg"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const AUTH_API = 'https://ro2.ani19962004.top/api/auth.php';
|
|
||||||
|
|
||||||
function toggleMobileSearch(show) {
|
|
||||||
if (show) {
|
|
||||||
document.body.classList.add('search-active');
|
|
||||||
setTimeout(() => document.getElementById('masthead-search-term').focus(), 100);
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('search-active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringToHeaderColor(str) {
|
|
||||||
let hash = 0; for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
const c = (hash & 0x00FFFFFF).toString(16).toUpperCase();
|
|
||||||
return '#' + '00000'.substring(0, 6 - c.length) + c;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHeaderLetterAvatar(name, container) {
|
|
||||||
const letter = name ? name.charAt(0) : 'U';
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'header-avatar-letter';
|
|
||||||
div.textContent = letter;
|
|
||||||
div.style.backgroundColor = stringToHeaderColor(name || 'User');
|
|
||||||
container.appendChild(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const userStr = localStorage.getItem('anime_user');
|
|
||||||
const guestBtns = document.getElementById('guest-buttons');
|
|
||||||
const userInfo = document.getElementById('user-info');
|
|
||||||
|
|
||||||
if(userStr) {
|
|
||||||
const user = JSON.parse(userStr);
|
|
||||||
if (guestBtns) guestBtns.style.display = 'none';
|
|
||||||
if (userInfo) {
|
|
||||||
userInfo.style.display = 'flex';
|
|
||||||
const avatarWrap = document.getElementById('header-avatar-link');
|
|
||||||
const avatarImg = document.getElementById('header-user-avatar');
|
|
||||||
const oldLetter = avatarWrap.querySelector('.header-avatar-letter');
|
|
||||||
if(oldLetter) oldLetter.remove();
|
|
||||||
|
|
||||||
if (user.avatar_path && user.avatar_path.length > 5) {
|
|
||||||
avatarImg.style.display = 'block';
|
|
||||||
avatarImg.src = user.avatar_path;
|
|
||||||
avatarImg.onerror = function() {
|
|
||||||
this.style.display = 'none';
|
|
||||||
createHeaderLetterAvatar(user.name, avatarWrap);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
avatarImg.style.display = 'none';
|
|
||||||
createHeaderLetterAvatar(user.name, avatarWrap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (guestBtns) guestBtns.style.display = 'flex';
|
|
||||||
if (userInfo) userInfo.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function showAuthModal(type) { document.getElementById('auth-modal').style.display = 'block'; switchAuth(type); }
|
|
||||||
function closeAuthModal() { document.getElementById('auth-modal').style.display = 'none'; }
|
|
||||||
function switchAuth(type) { document.getElementById('form-login').style.display = (type=='login')?'block':'none'; document.getElementById('form-register').style.display = (type=='register')?'block':'none'; document.getElementById('auth-msg').textContent = ''; }
|
|
||||||
async function handleLogin(){ /* ... ... */ }
|
|
||||||
async function handleRegister(){ /* ... ... */ }
|
|
||||||
|
|
||||||
function doLogout(){ if(confirm('로그아웃 ? 0.o ?')){localStorage.removeItem('anime_user'); window.location.href='/';}}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="body">
|
|
||||||
<title>원펀맨 애니 자막 Linkkf</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
#primary .entry-header {
|
|
||||||
background: none; border: none; padding: 0 0 15px 0;
|
|
||||||
margin-bottom: 25px; border-bottom: 3px solid #0073aa;
|
|
||||||
display: flex; justify-content: space-between; align-items: flex-end;
|
|
||||||
}
|
|
||||||
#primary .entry-title {
|
|
||||||
color: #ffffff; font-size: 2.2em; font-weight: 700;
|
|
||||||
margin: 0; padding: 0; line-height: 1.3;
|
|
||||||
}
|
|
||||||
#primary .search-total-count {
|
|
||||||
font-size: 1.1em; font-weight: 500; color: #aaa; margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.external-search-container {
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
margin: 30px auto;
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#external-search-form {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#external-search-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #121212;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #303030;
|
|
||||||
border-right: none;
|
|
||||||
border-radius: 40px 0 0 40px;
|
|
||||||
font-size: 1.1em;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#external-search-input:focus {
|
|
||||||
border-color: #1c62b9;
|
|
||||||
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#external-search-form button {
|
|
||||||
padding: 10px 25px;
|
|
||||||
background-color: #222222;
|
|
||||||
color: #aaa;
|
|
||||||
border: 1px solid #303030;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: background-color 0.2s, color 0.2s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#external-search-form button:hover {
|
|
||||||
background-color: #333333;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.ext-json-grid { display: grid; gap: 15px; margin-top: 20px; grid-template-columns: repeat(2, 1fr); }
|
|
||||||
@media (min-width: 768px) { .ext-json-grid { grid-template-columns: repeat(4, 1fr); } }
|
|
||||||
|
|
||||||
.ext-json-grid .loading, .ext-json-grid .error {
|
|
||||||
font-style: italic; color: #777; font-size: 1.2em; padding: 20px 0; grid-column: 1 / -1; text-align: center;
|
|
||||||
}
|
|
||||||
.ext-json-grid .error { color: #d9534f; }
|
|
||||||
|
|
||||||
|
|
||||||
.ext-json-item { background-color: #2a2a2a; border-radius: 8px; overflow: hidden; border: 1px solid #333; transition: transform 0.2s ease; }
|
|
||||||
.ext-json-item:hover { transform: translateY(-5px); }
|
|
||||||
.ext-json-item a { text-decoration: none; }
|
|
||||||
|
|
||||||
.ext-json-thumb { position: relative; display: block; aspect-ratio: 16 / 9; background-color: #1a1a1a; }
|
|
||||||
.ext-json-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
||||||
|
|
||||||
.ext-json-noti { position: absolute; top: 8px; left: 8px; background-color: #5bb7fe; color: white; padding: 3px 8px; border-radius: 4px; font-size: 0.8em; font-weight: bold; z-index: 2; }
|
|
||||||
|
|
||||||
.ext-json-info { padding: 12px; }
|
|
||||||
.ext-json-name { font-size: 0.9em; color: #eee; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
|
|
||||||
.ext-json-info .ext-json-meta { font-size: 0.8em; color: #999; margin-top: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
.ext-json-info .ext-json-meta .ext-json-season { margin-left: 5px; padding-left: 5px; border-left: 1px solid #555; }
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes skeleton-shimmer {
|
|
||||||
0% { background-position: -200% 0; }
|
|
||||||
100% { background-position: 200% 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-item {
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-animate {
|
|
||||||
background: #2a2a2a;
|
|
||||||
background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: skeleton-shimmer 1.5s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-thumb {
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-info { padding: 12px; }
|
|
||||||
|
|
||||||
.skeleton-text {
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-title { width: 85%; height: 18px; margin-bottom: 12px; }
|
|
||||||
.skeleton-meta { width: 50%; }
|
|
||||||
/* ---------------------------- */
|
|
||||||
|
|
||||||
/* Pagination */
|
|
||||||
#pagination-container.pagination-dark { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 30px; padding: 15px; background: #2a2a2a; border-radius: 8px; font-family: Arial, sans-serif; border: 1px solid #333; }
|
|
||||||
#pagination-container.pagination-dark button { padding: 12px 12px; background-color: #444; color: #eee; border: 1px solid #555; border-radius: 4px; cursor: pointer; font-size: 0.9em; }
|
|
||||||
#pagination-container.pagination-dark button:hover:not(:disabled) { background-color: #0073aa; color: #fff; border-color: #0073aa; }
|
|
||||||
#pagination-container.pagination-dark button:disabled { background-color: #333; color: #777; border-color: #444; cursor: not-allowed; }
|
|
||||||
#pagination-container.pagination-dark .page-info { color: #ccc; font-size: 0.9em; }
|
|
||||||
#pagination-container.pagination-dark input[type="number"] { width: 50px; padding: 7px; text-align: center; background-color: #333; color: #fff; border: 1px solid #555; border-radius: 4px; margin: 0 5px; -moz-appearance: textfield; }
|
|
||||||
#pagination-container.pagination-dark input[type="number"]::-webkit-outer-spin-button, #pagination-container.pagination-dark input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="primary" class="content-area">
|
|
||||||
<main id="main" class="site-main">
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<header class="entry-header">
|
|
||||||
<h1 class="entry-title" id="search-page-title">검색: 원펀맨</h1>
|
|
||||||
<span class="search-total-count" id="search-total-count-display" style="display: none;"></span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="entry-content">
|
|
||||||
|
|
||||||
<div class="external-search-container">
|
|
||||||
<form id="external-search-form" action="https://linkkf.live/" method="get">
|
|
||||||
<input type="text" name="s" id="external-search-input"
|
|
||||||
placeholder="......"
|
|
||||||
value="원펀맨" required>
|
|
||||||
<button type="submit"> 검색 </button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="results-grid" class="ext-json-grid">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="pagination-container" class="pagination-dark" style="display: none;">
|
|
||||||
<button id="page-first" disabled>« « «</button>
|
|
||||||
<button id="page-prev" disabled>‹ </button>
|
|
||||||
<span class="page-info">
|
|
||||||
page <input type="number" id="page-input" min="1" value="1" disabled> / <span id="page-total">1</span>
|
|
||||||
</span>
|
|
||||||
<button id="page-next" disabled> ›</button>
|
|
||||||
<button id="page-last" disabled> » » »</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
|
|
||||||
const API_SEARCH_URL = 'https://linkkf.5imgdarr.top/api/search.php';
|
|
||||||
const POSTS_PER_PAGE = 20;
|
|
||||||
|
|
||||||
// DOM Elements
|
|
||||||
const searchForm = document.getElementById('external-search-form');
|
|
||||||
const searchInput = document.getElementById('external-search-input');
|
|
||||||
const pageTitle = document.getElementById('search-page-title');
|
|
||||||
|
|
||||||
const resultsContainer = document.getElementById('results-grid');
|
|
||||||
const paginationContainer = document.getElementById('pagination-container');
|
|
||||||
const btnFirst = document.getElementById('page-first');
|
|
||||||
const btnPrev = document.getElementById('page-prev');
|
|
||||||
const btnNext = document.getElementById('page-next');
|
|
||||||
const btnLast = document.getElementById('page-last');
|
|
||||||
const pageInput = document.getElementById('page-input');
|
|
||||||
const pageTotal = document.getElementById('page-total');
|
|
||||||
const totalCountDisplay = document.getElementById('search-total-count-display');
|
|
||||||
|
|
||||||
let currentKeyword = "원펀맨";
|
|
||||||
let currentPage = 1;
|
|
||||||
let totalPages = 1;
|
|
||||||
|
|
||||||
|
|
||||||
function showSkeleton() {
|
|
||||||
|
|
||||||
let skeletonHTML = '';
|
|
||||||
for(let i=0; i<8; i++) {
|
|
||||||
skeletonHTML += `
|
|
||||||
<div class="skeleton-item">
|
|
||||||
<div class="skeleton-thumb skeleton-animate"></div>
|
|
||||||
<div class="skeleton-info">
|
|
||||||
<div class="skeleton-text skeleton-title skeleton-animate"></div>
|
|
||||||
<div class="skeleton-text skeleton-meta skeleton-animate"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
resultsContainer.innerHTML = skeletonHTML;
|
|
||||||
}
|
|
||||||
// ---------------------------------
|
|
||||||
|
|
||||||
function getPageFromURL() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const pageFromUrl = parseInt(urlParams.get('page'), 10);
|
|
||||||
if (!isNaN(pageFromUrl) && pageFromUrl > 0) return pageFromUrl;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateURL(keyword, page) {
|
|
||||||
const newUrl = new URL(window.location.href);
|
|
||||||
newUrl.searchParams.set('s', keyword);
|
|
||||||
|
|
||||||
if (page > 1) {
|
|
||||||
newUrl.searchParams.set('page', page);
|
|
||||||
} else {
|
|
||||||
newUrl.searchParams.delete('page');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newUrl.href !== window.location.href) {
|
|
||||||
history.pushState({s: keyword, page: page}, '', newUrl.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
pageTitle.textContent = ' o.o? ' + keyword;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchResults(page) {
|
|
||||||
if (!currentKeyword) {
|
|
||||||
resultsContainer.innerHTML = '<p class="error">....</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page < 1) page = 1;
|
|
||||||
if (page > totalPages && totalPages > 1) page = totalPages;
|
|
||||||
currentPage = page;
|
|
||||||
|
|
||||||
updateURL(currentKeyword, page);
|
|
||||||
|
|
||||||
showSkeleton();
|
|
||||||
|
|
||||||
paginationContainer.style.display = 'none';
|
|
||||||
totalCountDisplay.style.display = 'none';
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('keyword', currentKeyword);
|
|
||||||
params.append('page', page);
|
|
||||||
params.append('limit', POSTS_PER_PAGE);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_SEARCH_URL}?${params.toString()}`);
|
|
||||||
if (!response.ok) { throw new Error(` HTTP: ${response.status}`); }
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.status === 'success') {
|
|
||||||
displayResults(data.data);
|
|
||||||
displayPagination(data.pagination);
|
|
||||||
|
|
||||||
if (data.pagination.total_results != null) {
|
|
||||||
totalCountDisplay.textContent = `( + ${data.pagination.total_results} anime )`;
|
|
||||||
totalCountDisplay.style.display = 'block';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resultsContainer.innerHTML = '<p class="error"> ....</p>';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(' :', error);
|
|
||||||
resultsContainer.innerHTML = `<p class="error"> ....0.o </p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayResults(items) {
|
|
||||||
if (!items || items.length === 0) {
|
|
||||||
resultsContainer.innerHTML = '<p class="error"> ...</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let htmlOutput = '';
|
|
||||||
items.forEach(item => {
|
|
||||||
const postUrl = `https://linkkf.live/${item.postid}`;
|
|
||||||
const thumbHtml = item.thumb ? `<img src="https://rez1.ims1.top/350x/${item.thumb}" alt="${item.name}" loading="lazy">` : '';
|
|
||||||
const notiHtml = item.postnoti ? `<span class="ext-json-noti">${item.postnoti}</span>` : '';
|
|
||||||
|
|
||||||
const year = item.year || 'N/A';
|
|
||||||
const season = item.seasontype || 'N/A';
|
|
||||||
|
|
||||||
htmlOutput += `
|
|
||||||
<div class="ext-json-item">
|
|
||||||
<a href="${postUrl}" title="${item.name}">
|
|
||||||
<div class="ext-json-thumb">${thumbHtml}${notiHtml}</div>
|
|
||||||
<div class="ext-json-info">
|
|
||||||
<h3 class="ext-json-name">${item.name}</h3>
|
|
||||||
<div class="ext-json-meta">
|
|
||||||
<span class="ext-json-year">${year}</span>
|
|
||||||
<span class="ext-json-season">${season}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
resultsContainer.innerHTML = htmlOutput;
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayPagination(pagination) {
|
|
||||||
const { current_page, total_pages } = pagination;
|
|
||||||
currentPage = current_page; totalPages = total_pages;
|
|
||||||
if (total_pages <= 1) { paginationContainer.style.display = 'none'; return; }
|
|
||||||
|
|
||||||
paginationContainer.style.display = 'flex';
|
|
||||||
pageInput.value = current_page; pageInput.max = total_pages; pageTotal.textContent = total_pages;
|
|
||||||
|
|
||||||
const isFirstPage = (current_page === 1);
|
|
||||||
btnFirst.disabled = isFirstPage; btnPrev.disabled = isFirstPage;
|
|
||||||
const isLastPage = (current_page === total_pages);
|
|
||||||
btnNext.disabled = isLastPage; btnLast.disabled = isLastPage;
|
|
||||||
pageInput.disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchForm.addEventListener('submit', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const newKeyword = searchInput.value.trim();
|
|
||||||
if (newKeyword) {
|
|
||||||
currentKeyword = newKeyword;
|
|
||||||
fetchResults(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
btnFirst.addEventListener('click', () => { if (currentPage > 1) fetchResults(1); });
|
|
||||||
btnPrev.addEventListener('click', () => { if (currentPage > 1) fetchResults(currentPage - 1); });
|
|
||||||
btnNext.addEventListener('click', () => { if (currentPage < totalPages) fetchResults(currentPage + 1); });
|
|
||||||
btnLast.addEventListener('click', () => { if (currentPage < totalPages) fetchResults(totalPages); });
|
|
||||||
pageInput.addEventListener('keydown', (event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
let targetPage = parseInt(pageInput.value, 10);
|
|
||||||
if (isNaN(targetPage)) { pageInput.value = currentPage; return; }
|
|
||||||
if (targetPage < 1) targetPage = 1;
|
|
||||||
if (targetPage > totalPages) targetPage = totalPages;
|
|
||||||
fetchResults(targetPage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
window.addEventListener('popstate', function(event) {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const page = parseInt(urlParams.get('page'), 10) || 1;
|
|
||||||
const keyword = urlParams.get('s') || '';
|
|
||||||
|
|
||||||
if (keyword) {
|
|
||||||
currentKeyword = keyword;
|
|
||||||
searchInput.value = keyword;
|
|
||||||
|
|
||||||
|
|
||||||
showSkeleton();
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('keyword', currentKeyword);
|
|
||||||
params.append('page', page);
|
|
||||||
params.append('limit', POSTS_PER_PAGE);
|
|
||||||
|
|
||||||
fetch(`${API_SEARCH_URL}?${params.toString()}`)
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
displayResults(data.data);
|
|
||||||
displayPagination(data.pagination);
|
|
||||||
if (data.pagination.total_results != null) {
|
|
||||||
totalCountDisplay.textContent = `( + ${data.pagination.total_results} anime )`;
|
|
||||||
totalCountDisplay.style.display = 'block';
|
|
||||||
}
|
|
||||||
} else { resultsContainer.innerHTML = '<p class="error"> 0o0 .</p>'; }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialPage = getPageFromURL();
|
|
||||||
if (currentKeyword) {
|
|
||||||
fetchResults(initialPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<nav id="head-menu" role="navigation">
|
|
||||||
<ul>
|
|
||||||
|
|
||||||
<li> <a href="/" rel="home"style="
|
|
||||||
height: 30px;"> <span style='font-size:13px;'>😀</span> </br> <strong>메인</strong></a> </li>
|
|
||||||
<li> <a href="https://linkkf.live/anime-list/" rel="home"style="
|
|
||||||
height: 30px;
|
|
||||||
"> <span style='font-size:13px;'>😐</span> </br> <strong>신작</strong></a> </li>
|
|
||||||
|
|
||||||
<li> <a href="https://linkkf.live/menulist" rel="home"style="
|
|
||||||
height: 30px;
|
|
||||||
"> <span style='font-size:13px;'>😶</span> </br> <strong>장르</strong></a> </li>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li> <a href="https://linkkf.live/topanime" rel="home"style="
|
|
||||||
height: 30px;
|
|
||||||
"> <span style='font-size:13px;'>🌟</span> </br> <strong>TOP</strong></a> </li>
|
|
||||||
|
|
||||||
|
|
||||||
<li> <a href="https://linkkf.tv" rel="home"style="
|
|
||||||
height: 30px;
|
|
||||||
"> <span style='font-size:13px;'> 😆 </span> </br> <strong>링크2</strong></a> </li>
|
|
||||||
|
|
||||||
</ul></nav>
|
|
||||||
|
|
||||||
|
|
||||||
<article style="height:53px;width:100%px;">
|
|
||||||
|
|
||||||
</article>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="toast-notification" class="toast-container">
|
|
||||||
<div class="toast-icon"></div>
|
|
||||||
<div class="toast-message"> 0.o </div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
.toast-container {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 130px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%) translateY(100px);
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
color: #fff;
|
|
||||||
padding: 12px 25px;
|
|
||||||
border-radius: 50px;
|
|
||||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
z-index: 9999;
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
|
||||||
border: 1px solid #333;
|
|
||||||
min-width: 250px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-container.show {
|
|
||||||
transform: translateX(-50%) translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-icon {
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.toast-success .toast-icon { color: #4caf50; }
|
|
||||||
.toast-error .toast-icon { color: #f44336; }
|
|
||||||
.toast-info .toast-icon { color: #2196f3; }
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -112,6 +112,9 @@ setting = {
|
|||||||
|
|
||||||
from plugin import *
|
from plugin import *
|
||||||
import os
|
import os
|
||||||
|
import importlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
@@ -144,18 +147,44 @@ DEFINE_DEV = True
|
|||||||
|
|
||||||
P = create_plugin_instance(setting)
|
P = create_plugin_instance(setting)
|
||||||
|
|
||||||
# curl_cffi 자동 설치 루틴
|
REQUIRED_PACKAGES = [
|
||||||
try:
|
{"import_name": "bs4", "package_name": "beautifulsoup4"},
|
||||||
import curl_cffi
|
{"import_name": "requests_cache", "package_name": "requests-cache"},
|
||||||
except ImportError:
|
{"import_name": "cloudscraper", "package_name": "cloudscraper"},
|
||||||
|
{"import_name": "PIL", "package_name": "Pillow"},
|
||||||
|
{"import_name": "lxml", "package_name": "lxml"},
|
||||||
|
{"import_name": "aiohttp", "package_name": "aiohttp"},
|
||||||
|
{"import_name": "jsbeautifier", "package_name": "jsbeautifier"},
|
||||||
|
{"import_name": "curl_cffi", "package_name": "curl-cffi"},
|
||||||
|
{"import_name": "loguru", "package_name": "loguru"},
|
||||||
|
{"import_name": "zendriver", "package_name": "zendriver==0.15.3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dependencies():
|
||||||
|
missing_packages = []
|
||||||
|
for dependency in REQUIRED_PACKAGES:
|
||||||
|
try:
|
||||||
|
importlib.import_module(dependency["import_name"])
|
||||||
|
except ImportError:
|
||||||
|
missing_packages.append(dependency)
|
||||||
|
|
||||||
|
if not missing_packages:
|
||||||
|
return
|
||||||
|
|
||||||
|
package_names = [dependency["package_name"] for dependency in missing_packages]
|
||||||
|
P.logger.warning(f"Missing plugin dependencies detected: {package_names}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import subprocess
|
subprocess.check_call([sys.executable, "-m", "pip", "install"] + package_names)
|
||||||
import sys
|
P.logger.info(f"Plugin dependencies installed successfully: {package_names}")
|
||||||
P.logger.info("curl_cffi not found. Attempting to install...")
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "curl-cffi"])
|
|
||||||
P.logger.info("curl_cffi installed successfully.")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error(f"Failed to install curl_cffi: {e}")
|
P.logger.error(f"Failed to install plugin dependencies: {package_names}")
|
||||||
|
P.logger.error(f"Install error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
ensure_dependencies()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if DEFINE_DEV:
|
if DEFINE_DEV:
|
||||||
@@ -180,4 +209,3 @@ try:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error(f'Exception: {str(e)}')
|
P.logger.error(f'Exception: {str(e)}')
|
||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|||||||
-360
@@ -1,360 +0,0 @@
|
|||||||
{
|
|
||||||
"cells": [
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 1,
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": true
|
|
||||||
},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"<class 'datetime.date'>\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"from datetime import date, datetime\n",
|
|
||||||
"\n",
|
|
||||||
"print(date)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 2,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"2022-06-07\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"print(date.today())"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 4,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"ename": "RuntimeError",
|
|
||||||
"evalue": "asyncio.run() cannot be called from a running event loop",
|
|
||||||
"output_type": "error",
|
|
||||||
"traceback": [
|
|
||||||
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
|
|
||||||
"\u001B[0;31mRuntimeError\u001B[0m Traceback (most recent call last)",
|
|
||||||
"\u001B[0;32m/var/folders/9l/5sls926d02g2kzqb70_0c7rc0000gn/T/ipykernel_40530/2714877972.py\u001B[0m in \u001B[0;36m<module>\u001B[0;34m\u001B[0m\n\u001B[1;32m 52\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 53\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 54\u001B[0;31m \u001B[0masyncio\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mrun\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mmain\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/asyncio/runners.py\u001B[0m in \u001B[0;36mrun\u001B[0;34m(main, debug)\u001B[0m\n\u001B[1;32m 31\u001B[0m \"\"\"\n\u001B[1;32m 32\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mevents\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_get_running_loop\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m \u001B[0;32mis\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0;32mNone\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 33\u001B[0;31m raise RuntimeError(\n\u001B[0m\u001B[1;32m 34\u001B[0m \"asyncio.run() cannot be called from a running event loop\")\n\u001B[1;32m 35\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;31mRuntimeError\u001B[0m: asyncio.run() cannot be called from a running event loop"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"from playwright.sync_api import sync_playwright\n",
|
|
||||||
"from playwright.async_api import async_playwright\n",
|
|
||||||
"# from playwright_stealth import stealth_sync\n",
|
|
||||||
"import asyncio\n",
|
|
||||||
"\n",
|
|
||||||
"async def run(pw):\n",
|
|
||||||
"\n",
|
|
||||||
" browser = await pw.chromium.launch(headless=False)\n",
|
|
||||||
" # context = browser.new_context(\n",
|
|
||||||
" # user_agent=ua,\n",
|
|
||||||
" # )\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
" url = 'https://anilife.live/h/live?p=5aaf4907-da62-4b98-ba8f-59066a0dc0f4&a=none&player=jawcloud'\n",
|
|
||||||
" #\n",
|
|
||||||
" # if referer is not None:\n",
|
|
||||||
" # LogicAniLife.headers[\"Referer\"] = referer\n",
|
|
||||||
"\n",
|
|
||||||
" # context = browser.new_context(extra_http_headers=LogicAniLife.headers)\n",
|
|
||||||
" context = await browser.new_context()\n",
|
|
||||||
" # LogicAniLife.headers[\"Cookie\"] = cookie_value\n",
|
|
||||||
"\n",
|
|
||||||
" # context.set_extra_http_headers(LogicAniLife.headers)\n",
|
|
||||||
"\n",
|
|
||||||
" page = await context.new_page()\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
" # page.on(\"request\", set_cookie)\n",
|
|
||||||
" # stealth_sync(page)\n",
|
|
||||||
" page.goto(\n",
|
|
||||||
" url, wait_until=\"networkidle\"\n",
|
|
||||||
" )\n",
|
|
||||||
" page.wait_for_timeout(10000)\n",
|
|
||||||
" # time.sleep(1)\n",
|
|
||||||
" # page.reload()\n",
|
|
||||||
"\n",
|
|
||||||
" # time.sleep(10)\n",
|
|
||||||
" cookies = context.cookies\n",
|
|
||||||
" # print(cookies)\n",
|
|
||||||
"\n",
|
|
||||||
" # print(page.content())\n",
|
|
||||||
" vod_url = page.evaluate(\n",
|
|
||||||
" \"\"\"() => {\n",
|
|
||||||
" return console.log(vodUrl_1080p) }\"\"\"\n",
|
|
||||||
" )\n",
|
|
||||||
" print(vod_url)\n",
|
|
||||||
"\n",
|
|
||||||
"async def main():\n",
|
|
||||||
" async with async_playwright() as p:\n",
|
|
||||||
" await main_run(p)\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"asyncio.run(main())"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 5,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"ename": "Error",
|
|
||||||
"evalue": "Executable doesn't exist at /Users/yommi/Library/Caches/ms-playwright/chromium-939194/chrome-mac/Chromium.app/Contents/MacOS/Chromium\n╔═════════════════════════════════════════════════════════════════════════╗\n║ Looks like Playwright Test or Playwright was just installed or updated. ║\n║ Please run the following command to download new browsers: ║\n║ ║\n║ playwright install ║\n║ ║\n║ <3 Playwright Team ║\n╚═════════════════════════════════════════════════════════════════════════╝",
|
|
||||||
"output_type": "error",
|
|
||||||
"traceback": [
|
|
||||||
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
|
|
||||||
"\u001B[0;31mError\u001B[0m Traceback (most recent call last)",
|
|
||||||
"\u001B[0;32m/var/folders/9l/5sls926d02g2kzqb70_0c7rc0000gn/T/ipykernel_67283/2756312828.py\u001B[0m in \u001B[0;36m<module>\u001B[0;34m\u001B[0m\n\u001B[1;32m 57\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 58\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 59\u001B[0;31m \u001B[0masyncio\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mrun\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mmain\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/site-packages/nest_asyncio.py\u001B[0m in \u001B[0;36mrun\u001B[0;34m(future, debug)\u001B[0m\n\u001B[1;32m 30\u001B[0m \u001B[0mloop\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0masyncio\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mget_event_loop\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 31\u001B[0m \u001B[0mloop\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mset_debug\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mdebug\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 32\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mloop\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mrun_until_complete\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mfuture\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 33\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 34\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0msys\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mversion_info\u001B[0m \u001B[0;34m>=\u001B[0m \u001B[0;34m(\u001B[0m\u001B[0;36m3\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;36m6\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;36m0\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/site-packages/nest_asyncio.py\u001B[0m in \u001B[0;36mrun_until_complete\u001B[0;34m(self, future)\u001B[0m\n\u001B[1;32m 68\u001B[0m raise RuntimeError(\n\u001B[1;32m 69\u001B[0m 'Event loop stopped before Future completed.')\n\u001B[0;32m---> 70\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mf\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mresult\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 71\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 72\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0m_run_once\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/asyncio/futures.py\u001B[0m in \u001B[0;36mresult\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 176\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m__log_traceback\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;32mFalse\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 177\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_exception\u001B[0m \u001B[0;32mis\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0;32mNone\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 178\u001B[0;31m \u001B[0;32mraise\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_exception\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 179\u001B[0m \u001B[0;32mreturn\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_result\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 180\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/asyncio/tasks.py\u001B[0m in \u001B[0;36m__step\u001B[0;34m(***failed resolving arguments***)\u001B[0m\n\u001B[1;32m 278\u001B[0m \u001B[0;31m# We use the `send` method directly, because coroutines\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 279\u001B[0m \u001B[0;31m# don't have `__iter__` and `__next__` methods.\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 280\u001B[0;31m \u001B[0mresult\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mcoro\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0msend\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 281\u001B[0m \u001B[0;32melse\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 282\u001B[0m \u001B[0mresult\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mcoro\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mthrow\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mexc\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/var/folders/9l/5sls926d02g2kzqb70_0c7rc0000gn/T/ipykernel_67283/2756312828.py\u001B[0m in \u001B[0;36mmain\u001B[0;34m()\u001B[0m\n\u001B[1;32m 54\u001B[0m \u001B[0;32masync\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mmain\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 55\u001B[0m \u001B[0;32masync\u001B[0m \u001B[0;32mwith\u001B[0m \u001B[0masync_playwright\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m \u001B[0;32mas\u001B[0m \u001B[0mp\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 56\u001B[0;31m \u001B[0;32mawait\u001B[0m \u001B[0mmain_run\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mp\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 57\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 58\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/var/folders/9l/5sls926d02g2kzqb70_0c7rc0000gn/T/ipykernel_67283/2756312828.py\u001B[0m in \u001B[0;36mmain_run\u001B[0;34m(playwright)\u001B[0m\n\u001B[1;32m 11\u001B[0m \u001B[0;32masync\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mmain_run\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mplaywright\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 12\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 13\u001B[0;31m \u001B[0mbrowser\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;32mawait\u001B[0m \u001B[0mplaywright\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mchromium\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mlaunch\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mheadless\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mFalse\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 14\u001B[0m \u001B[0;31m# context = browser.new_context(\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 15\u001B[0m \u001B[0;31m# user_agent=ua,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/site-packages/playwright/async_api/_generated.py\u001B[0m in \u001B[0;36mlaunch\u001B[0;34m(self, executable_path, channel, args, ignore_default_args, handle_sigint, handle_sigterm, handle_sighup, timeout, env, headless, devtools, proxy, downloads_path, slow_mo, traces_dir, chromium_sandbox, firefox_user_prefs)\u001B[0m\n\u001B[1;32m 11600\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 11601\u001B[0m return mapping.from_impl(\n\u001B[0;32m> 11602\u001B[0;31m await self._async(\n\u001B[0m\u001B[1;32m 11603\u001B[0m \u001B[0;34m\"browser_type.launch\"\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 11604\u001B[0m self._impl_obj.launch(\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/site-packages/playwright/_impl/_browser_type.py\u001B[0m in \u001B[0;36mlaunch\u001B[0;34m(self, executablePath, channel, args, ignoreDefaultArgs, handleSIGINT, handleSIGTERM, handleSIGHUP, timeout, env, headless, devtools, proxy, downloadsPath, slowMo, tracesDir, chromiumSandbox, firefoxUserPrefs)\u001B[0m\n\u001B[1;32m 86\u001B[0m \u001B[0mparams\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mlocals_to_params\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mlocals\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 87\u001B[0m \u001B[0mnormalize_launch_params\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 88\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mfrom_channel\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;32mawait\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_channel\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0msend\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m\"launch\"\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 89\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 90\u001B[0m async def launch_persistent_context(\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/site-packages/playwright/_impl/_connection.py\u001B[0m in \u001B[0;36msend\u001B[0;34m(self, method, params)\u001B[0m\n\u001B[1;32m 37\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 38\u001B[0m \u001B[0;32masync\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0msend\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmethod\u001B[0m\u001B[0;34m:\u001B[0m \u001B[0mstr\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m:\u001B[0m \u001B[0mDict\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m \u001B[0;34m->\u001B[0m \u001B[0mAny\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 39\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0;32mawait\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0minner_send\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mmethod\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;32mFalse\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 40\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 41\u001B[0m \u001B[0;32masync\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0msend_return_as_dict\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmethod\u001B[0m\u001B[0;34m:\u001B[0m \u001B[0mstr\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m:\u001B[0m \u001B[0mDict\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m \u001B[0;34m->\u001B[0m \u001B[0mAny\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/site-packages/playwright/_impl/_connection.py\u001B[0m in \u001B[0;36minner_send\u001B[0;34m(self, method, params, return_as_dict)\u001B[0m\n\u001B[1;32m 61\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0mcallback\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mfuture\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mdone\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 62\u001B[0m \u001B[0mcallback\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mfuture\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mcancel\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m---> 63\u001B[0;31m \u001B[0mresult\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mnext\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0miter\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mdone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mresult\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 64\u001B[0m \u001B[0;31m# Protocol now has named return values, assume result is one level deeper unless\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 65\u001B[0m \u001B[0;31m# there is explicit ambiguity.\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;32m/Volumes/WD/Users/yommi/miniforge3/envs/ani-downloader/lib/python3.8/asyncio/futures.py\u001B[0m in \u001B[0;36mresult\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 176\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m__log_traceback\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;32mFalse\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 177\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_exception\u001B[0m \u001B[0;32mis\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0;32mNone\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 178\u001B[0;31m \u001B[0;32mraise\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_exception\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 179\u001B[0m \u001B[0;32mreturn\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_result\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 180\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n",
|
|
||||||
"\u001B[0;31mError\u001B[0m: Executable doesn't exist at /Users/yommi/Library/Caches/ms-playwright/chromium-939194/chrome-mac/Chromium.app/Contents/MacOS/Chromium\n╔═════════════════════════════════════════════════════════════════════════╗\n║ Looks like Playwright Test or Playwright was just installed or updated. ║\n║ Please run the following command to download new browsers: ║\n║ ║\n║ playwright install ║\n║ ║\n║ <3 Playwright Team ║\n╚═════════════════════════════════════════════════════════════════════════╝"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"# !playwright install\n",
|
|
||||||
"\n",
|
|
||||||
"from playwright.sync_api import sync_playwright\n",
|
|
||||||
"from playwright.async_api import async_playwright\n",
|
|
||||||
"# from playwright_stealth import stealth_sync\n",
|
|
||||||
"import asyncio\n",
|
|
||||||
"import nest_asyncio\n",
|
|
||||||
"nest_asyncio.apply()\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"async def main_run(playwright):\n",
|
|
||||||
"\n",
|
|
||||||
" browser = await playwright.chromium.launch(headless=False)\n",
|
|
||||||
" # context = browser.new_context(\n",
|
|
||||||
" # user_agent=ua,\n",
|
|
||||||
" # )\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
" url = 'https://sir.kr'\n",
|
|
||||||
" #\n",
|
|
||||||
" # if referer is not None:\n",
|
|
||||||
" # LogicAniLife.headers[\"Referer\"] = referer\n",
|
|
||||||
"\n",
|
|
||||||
" # context = browser.new_context(extra_http_headers=LogicAniLife.headers)\n",
|
|
||||||
" context = await browser.new_context()\n",
|
|
||||||
" # LogicAniLife.headers[\"Cookie\"] = cookie_value\n",
|
|
||||||
"\n",
|
|
||||||
" # context.set_extra_http_headers(LogicAniLife.headers)\n",
|
|
||||||
"\n",
|
|
||||||
" page = await context.new_page()\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
" # page.on(\"request\", set_cookie)\n",
|
|
||||||
" # stealth_sync(page)\n",
|
|
||||||
" page.goto(\n",
|
|
||||||
" url, wait_until=\"networkidle\"\n",
|
|
||||||
" )\n",
|
|
||||||
" page.wait_for_timeout(10000)\n",
|
|
||||||
" # time.sleep(1)\n",
|
|
||||||
" # page.reload()\n",
|
|
||||||
"\n",
|
|
||||||
" # time.sleep(10)\n",
|
|
||||||
" cookies = context.cookies\n",
|
|
||||||
" # print(cookies)\n",
|
|
||||||
"\n",
|
|
||||||
" # print(page.content())\n",
|
|
||||||
" vod_url = page.evaluate(\n",
|
|
||||||
" \"\"\"() => {\n",
|
|
||||||
" return console.log(vodUrl_1080p) }\"\"\"\n",
|
|
||||||
" )\n",
|
|
||||||
" print(vod_url)\n",
|
|
||||||
"\n",
|
|
||||||
"async def main():\n",
|
|
||||||
" async with async_playwright() as p:\n",
|
|
||||||
" await main_run(p)\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"asyncio.run(main())"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 3,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"<module 'select' from '/Users/yommi/.pyenv/versions/3.10.7/lib/python3.10/lib-dynload/select.cpython-310-darwin.so'>\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"import select\n",
|
|
||||||
"\n",
|
|
||||||
"print(select)"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 4,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"['KQ_EV_ADD', 'KQ_EV_CLEAR', 'KQ_EV_DELETE', 'KQ_EV_DISABLE', 'KQ_EV_ENABLE', 'KQ_EV_EOF', 'KQ_EV_ERROR', 'KQ_EV_FLAG1', 'KQ_EV_ONESHOT', 'KQ_EV_SYSFLAGS', 'KQ_FILTER_AIO', 'KQ_FILTER_PROC', 'KQ_FILTER_READ', 'KQ_FILTER_SIGNAL', 'KQ_FILTER_TIMER', 'KQ_FILTER_VNODE', 'KQ_FILTER_WRITE', 'KQ_NOTE_ATTRIB', 'KQ_NOTE_CHILD', 'KQ_NOTE_DELETE', 'KQ_NOTE_EXEC', 'KQ_NOTE_EXIT', 'KQ_NOTE_EXTEND', 'KQ_NOTE_FORK', 'KQ_NOTE_LINK', 'KQ_NOTE_LOWAT', 'KQ_NOTE_PCTRLMASK', 'KQ_NOTE_PDATAMASK', 'KQ_NOTE_RENAME', 'KQ_NOTE_REVOKE', 'KQ_NOTE_TRACK', 'KQ_NOTE_TRACKERR', 'KQ_NOTE_WRITE', 'PIPE_BUF', 'POLLERR', 'POLLHUP', 'POLLIN', 'POLLNVAL', 'POLLOUT', 'POLLPRI', 'POLLRDBAND', 'POLLRDNORM', 'POLLWRBAND', 'POLLWRNORM', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'error', 'kevent', 'kqueue', 'poll', 'select']\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"print(dir(select))"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 6,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"_CountingAttr(counter=23, _default=Factory(factory=<class 'select.kqueue'>, takes_self=False), repr=True, eq=True, order=True, hash=None, init=True, on_setattr=None, metadata={})\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"import attr\n",
|
|
||||||
"\n",
|
|
||||||
"_kqueue = attr.ib(factory=select.kqueue)\n",
|
|
||||||
"\n",
|
|
||||||
"print(_kqueue)"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"outputs": [],
|
|
||||||
"source": [],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"outputs": [],
|
|
||||||
"source": [],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 1,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"용사 파티에서 추방된 비스트테이머, 최강종의 고양이귀 소녀와 만나다.S01E06.1080p-OHNI24.mp4\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"import os\n",
|
|
||||||
"\n",
|
|
||||||
"t_str = \"/data/anime_downloader/ohli24/용사 파티에서 추방된 비스트테이머, 최강종의 고양이귀 소녀와 만나다/Season 1/용사 파티에서 추방된 비스트테이머, 최강종의 고양이귀 소녀와 만나다.S01E06.1080p-OHNI24.mp4\"\n",
|
|
||||||
"\n",
|
|
||||||
"filename = os.path.basename(t_str)\n",
|
|
||||||
"print(filename)"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 2,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"thumbnails.vtt\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"srt_url = \"https://cdn1-nydoodle.xyz/cdn/down/e2dbe79957ca1ba094c80cfd4c7604cb/thumbnails.vtt\"\n",
|
|
||||||
"\n",
|
|
||||||
"print(srt_url.split('/')[-1])\n"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"kernelspec": {
|
|
||||||
"display_name": "Python 3",
|
|
||||||
"language": "python",
|
|
||||||
"name": "python3"
|
|
||||||
},
|
|
||||||
"language_info": {
|
|
||||||
"codemirror_mode": {
|
|
||||||
"name": "ipython",
|
|
||||||
"version": 2
|
|
||||||
},
|
|
||||||
"file_extension": ".py",
|
|
||||||
"mimetype": "text/x-python",
|
|
||||||
"name": "python",
|
|
||||||
"nbconvert_exporter": "python",
|
|
||||||
"pygments_lexer": "ipython2",
|
|
||||||
"version": "2.7.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nbformat": 4,
|
|
||||||
"nbformat_minor": 0
|
|
||||||
}
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def test_fetch():
|
|
||||||
url = "https://playv2.sub3.top/r2/play.php?&id=n20&url=405686s1"
|
|
||||||
headers = {
|
|
||||||
"Referer": "https://linkkf.live/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
|
|
||||||
}
|
|
||||||
|
|
||||||
daemon_url = "http://127.0.0.1:19876/fetch"
|
|
||||||
payload = {
|
|
||||||
"url": url,
|
|
||||||
"headers": headers,
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"Fetching {url} via daemon...")
|
|
||||||
try:
|
|
||||||
resp = requests.post(daemon_url, json=payload, timeout=40)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
print(f"Error: HTTP {resp.status_code}")
|
|
||||||
print(resp.text)
|
|
||||||
return
|
|
||||||
|
|
||||||
data = resp.json()
|
|
||||||
if not data.get("success"):
|
|
||||||
print(f"Fetch failed: {data.get('error')}")
|
|
||||||
return
|
|
||||||
|
|
||||||
html = data.get("html", "")
|
|
||||||
print(f"Fetch success. Length: {len(html)}")
|
|
||||||
|
|
||||||
# Save for inspection
|
|
||||||
with open("linkkf_player_test.html", "w", encoding="utf-8") as f:
|
|
||||||
f.write(html)
|
|
||||||
print("Saved to linkkf_player_test.html")
|
|
||||||
|
|
||||||
# Try regex patterns from mod_linkkf.py
|
|
||||||
patterns = [
|
|
||||||
r"url:\s*['\"]([^'\"]*\.m3u8[^'\"]*)['\"]",
|
|
||||||
r"<source[^>]+src=['\"]([^'\"]*\.m3u8[^'\"]*)['\"]",
|
|
||||||
r"src\s*=\s*['\"]([^'\"]*\.m3u8[^'\"]*)['\"]",
|
|
||||||
r"url\s*:\s*['\"]([^'\"]+)['\"]"
|
|
||||||
]
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for p in patterns:
|
|
||||||
match = re.search(p, html, re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
url_found = match.group(1)
|
|
||||||
if ".m3u8" in url_found or "m3u8" in p:
|
|
||||||
print(f"Pattern '{p}' found: {url_found}")
|
|
||||||
found = True
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
print("No m3u8 found with existing patterns.")
|
|
||||||
# Search for any .m3u8
|
|
||||||
any_m3u8 = re.findall(r"['\"]([^'\"]*\.m3u8[^'\"]*)['\"]", html)
|
|
||||||
if any_m3u8:
|
|
||||||
print(f"Generic search found {len(any_m3u8)} m3u8 links:")
|
|
||||||
for m in any_m3u8[:5]:
|
|
||||||
print(f" - {m}")
|
|
||||||
else:
|
|
||||||
print("No .m3u8 found in generic search either.")
|
|
||||||
# Check for other video extensions or potential indicators
|
|
||||||
if "Artplayer" in html:
|
|
||||||
print("Artplayer detected.")
|
|
||||||
if "video" in html:
|
|
||||||
print("Video tag found.")
|
|
||||||
|
|
||||||
# Check for 'cache/'
|
|
||||||
if "cache/" in html:
|
|
||||||
print("Found 'cache/' keyword.")
|
|
||||||
cache_links = re.findall(r"['\"]([^'\"]*cache/[^'\"]*)['\"]", html)
|
|
||||||
for c in cache_links:
|
|
||||||
print(f" - Possible cache link: {c}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Exception: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_fetch()
|
|
||||||
Reference in New Issue
Block a user