Sync all projects
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
# coding=utf-8
|
||||
# hanime1.py —— TVBox / FongMi Python 爬虫
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
host = 'https://hanime1.me'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://hanime1.me/',
|
||||
'Accept-Language': 'zh-TW,zh;q=0.9,zh-CN;q=0.8',
|
||||
}
|
||||
|
||||
cates = [
|
||||
('最新里番', 'genre_sort:裏番|最新上市'),
|
||||
('最新上市', 'sort:最新上市'),
|
||||
('最新上传', 'sort:最新上傳'),
|
||||
('里番', '裏番'),
|
||||
('泡面番', '泡麵番'),
|
||||
('Motion Anime', 'Motion Anime'),
|
||||
('3DCG', '3DCG'),
|
||||
('2.5D', '2.5D'),
|
||||
('2D动画', '2D動畫'),
|
||||
('AI生成', 'AI生成'),
|
||||
('MMD', 'MMD'),
|
||||
('Cosplay', 'Cosplay'),
|
||||
('新番预告', '新番預告'),
|
||||
]
|
||||
|
||||
sorts = [
|
||||
{'n': '最新上市', 'v': '最新上市'},
|
||||
{'n': '最新上传', 'v': '最新上傳'},
|
||||
{'n': '本日排行', 'v': '本日排行'},
|
||||
{'n': '本周排行', 'v': '本週排行'},
|
||||
{'n': '本月排行', 'v': '本月排行'},
|
||||
]
|
||||
|
||||
# ---------- 基础 ----------
|
||||
def getName(self):
|
||||
return 'hanime1'
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
return None
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
return self.fetch(url, headers=self.headers, timeout=15).text
|
||||
except Exception:
|
||||
import requests
|
||||
return requests.get(url, headers=self.headers, timeout=15).text
|
||||
|
||||
def _meta(self, html, prop):
|
||||
m = re.search(r'<meta[^>]+property="%s"[^>]+content="([^"]*)"' % prop, html)
|
||||
if not m:
|
||||
m = re.search(r'<meta[^>]+content="([^"]*)"[^>]+property="%s"' % prop, html)
|
||||
return m.group(1).strip() if m else ''
|
||||
|
||||
# ---------- 列表解析 ----------
|
||||
def parse_list(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
|
||||
card_pattern = re.compile(
|
||||
r'<a[^>]+href=["\'](?:https?://hanime1\.me)?/watch\?v=(\d+)["\'][^>]*>(.*?)</a>',
|
||||
re.S
|
||||
)
|
||||
|
||||
for m in card_pattern.finditer(html):
|
||||
vid = m.group(1)
|
||||
if vid in seen:
|
||||
continue
|
||||
inner = m.group(2)
|
||||
|
||||
title = ''
|
||||
t = re.search(
|
||||
r'class="[^"]*(?:title|name)[^"]*"[^>]*>\s*([^<]+?)\s*<',
|
||||
inner, re.S
|
||||
)
|
||||
if t:
|
||||
title = t.group(1).strip()
|
||||
if not title:
|
||||
t = re.search(r'<img[^>]+alt="([^"]+)"', inner)
|
||||
if t:
|
||||
title = t.group(1).strip()
|
||||
if not title:
|
||||
t = re.search(r'title="([^"]+)"', m.group(0))
|
||||
if t:
|
||||
title = t.group(1).strip()
|
||||
if not title:
|
||||
continue
|
||||
|
||||
pic = ''
|
||||
for img in re.findall(r'<img[^>]+(?:data-src|src)="(http[^"]+)"', inner):
|
||||
if '.gif' in img or 'icon' in img or 'avatar' in img:
|
||||
continue
|
||||
pic = img
|
||||
break
|
||||
|
||||
seen.add(vid)
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': ''
|
||||
})
|
||||
|
||||
if not videos:
|
||||
videos = self._parse_list_fallback(html)
|
||||
return videos
|
||||
|
||||
def _parse_list_fallback(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
for m in re.finditer(
|
||||
r'<a[^>]+href=["\'][^"\']*watch\?v=(\d+)["\']([^>]*)>(.*?)</a>',
|
||||
html, re.S
|
||||
):
|
||||
vid = m.group(1)
|
||||
if vid in seen:
|
||||
continue
|
||||
attrs = m.group(2)
|
||||
inner = m.group(3)
|
||||
t = re.search(r'title="([^"]+)"', attrs)
|
||||
title = t.group(1).strip() if t else ''
|
||||
if not title:
|
||||
title = re.sub(r'<[^>]+>', '', inner).strip()
|
||||
if not title:
|
||||
continue
|
||||
seen.add(vid)
|
||||
videos.append({'vod_id': vid, 'vod_name': title, 'vod_pic': '', 'vod_remarks': ''})
|
||||
return videos
|
||||
|
||||
# ---------- 首页 ----------
|
||||
def homeContent(self, filter):
|
||||
classes = [{'type_name': n, 'type_id': v} for n, v in self.cates]
|
||||
filters = {}
|
||||
for _, v in self.cates:
|
||||
if not v.startswith('sort:') and not v.startswith('genre_sort:'):
|
||||
filters[v] = [{'key': 'sort', 'name': '排序', 'value': self.sorts}]
|
||||
return {'class': classes, 'filters': filters}
|
||||
|
||||
def homeVideoContent(self):
|
||||
html = self._get(self.host)
|
||||
return {'list': self.parse_list(html)[:30]}
|
||||
|
||||
# ---------- 分类 ----------
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid.startswith('genre_sort:'):
|
||||
parts = tid[len('genre_sort:'):].split('|', 1)
|
||||
genre = parts[0]
|
||||
sort = parts[1] if len(parts) > 1 else ''
|
||||
url = '{}/search?genre={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(genre), pg)
|
||||
if sort:
|
||||
url += '&sort=' + urllib.parse.quote(sort)
|
||||
elif tid.startswith('sort:'):
|
||||
sort_val = tid[len('sort:'):]
|
||||
url = '{}/search?sort={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(sort_val), pg)
|
||||
else:
|
||||
url = '{}/search?genre={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(tid), pg)
|
||||
if extend and extend.get('sort'):
|
||||
url += '&sort=' + urllib.parse.quote(extend['sort'])
|
||||
|
||||
html = self._get(url)
|
||||
videos = self.parse_list(html)
|
||||
return {
|
||||
'list': videos,
|
||||
'page': int(pg),
|
||||
'pagecount': int(pg) + (1 if len(videos) >= 20 else 0),
|
||||
'limit': 30,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
# ---------- 详情(核心修复:把当前请求的这一集放到播放列表首位) ----------
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0]
|
||||
html = self._get('{}/watch?v={}'.format(self.host, vid))
|
||||
|
||||
title = self._meta(html, 'og:title')
|
||||
pic = self._meta(html, 'og:image')
|
||||
desc = self._meta(html, 'og:description')
|
||||
|
||||
episodes = self._extract_playlist(html, vid, title)
|
||||
|
||||
play_url = '#'.join(
|
||||
'{}${}'.format(ep['name'], ep['vid'])
|
||||
for ep in episodes
|
||||
)
|
||||
|
||||
vod = {
|
||||
'vod_id' : vid,
|
||||
'vod_name' : title,
|
||||
'vod_pic' : pic,
|
||||
'vod_content' : desc,
|
||||
'vod_play_from': 'Hanime1',
|
||||
'vod_play_url' : play_url,
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def _extract_playlist(self, html, current_vid, current_title):
|
||||
"""
|
||||
提取播放列表(选集),并把当前请求的这一集放到列表最前面,
|
||||
这样无论用户从外层列表点的是第几集,进详情页后默认播放的
|
||||
都是用户实际点击的那一集,而不是固定播放第一集。
|
||||
"""
|
||||
episodes = []
|
||||
seen = set()
|
||||
|
||||
ep_pattern = re.compile(
|
||||
r'<a class="overlay" href="https?://hanime1\.me/watch\?v=(\d+)"></a>\s*'
|
||||
r'<div class="card-mobile-panel inner">.*?'
|
||||
r'<div class="card-mobile-title"[^>]*>([^<]+)</div>',
|
||||
re.S
|
||||
)
|
||||
|
||||
for m in ep_pattern.finditer(html):
|
||||
ep_vid = m.group(1)
|
||||
ep_name = m.group(2).strip()
|
||||
if ep_vid in seen:
|
||||
continue
|
||||
seen.add(ep_vid)
|
||||
episodes.append({'name': ep_name, 'vid': ep_vid})
|
||||
|
||||
# 没有播放列表说明是单集视频,直接返回当前这一集
|
||||
if not episodes:
|
||||
return [{'name': current_title or current_vid, 'vid': current_vid}]
|
||||
|
||||
# 确保当前请求的这一集一定在列表里(理论上必然存在,兜底一下)
|
||||
if not any(e['vid'] == current_vid for e in episodes):
|
||||
episodes.append({'name': current_title or current_vid, 'vid': current_vid})
|
||||
|
||||
# 先按集数数字升序排好,方便用户在选集列表里查看顺序
|
||||
def _ep_num(ep):
|
||||
m = re.search(r'(\d+)\s*$', ep['name'])
|
||||
return int(m.group(1)) if m else 0
|
||||
|
||||
try:
|
||||
episodes.sort(key=_ep_num)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 核心修复:把当前请求的这一集挪到最前面,作为默认播放项
|
||||
current_index = next(
|
||||
(i for i, e in enumerate(episodes) if e['vid'] == current_vid), None
|
||||
)
|
||||
if current_index is not None and current_index != 0:
|
||||
current_ep = episodes.pop(current_index)
|
||||
episodes.insert(0, current_ep)
|
||||
|
||||
return episodes
|
||||
|
||||
# ---------- 搜索 ----------
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
url = '{}/search?query={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(key), pg)
|
||||
html = self._get(url)
|
||||
return {'list': self.parse_list(html), 'page': int(pg)}
|
||||
|
||||
# ---------- 播放 ----------
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
html = self._get('{}/watch?v={}'.format(self.host, id))
|
||||
play = ''
|
||||
sources = re.findall(r'<source[^>]+src="([^"]+)"[^>]*size="(\d+)"', html)
|
||||
if sources:
|
||||
sources.sort(key=lambda x: int(x[1]), reverse=True)
|
||||
play = sources[0][0]
|
||||
if not play:
|
||||
m = re.search(r'"contentUrl"\s*:\s*"([^"]+)"', html)
|
||||
if m:
|
||||
play = m.group(1).replace('\\/', '/')
|
||||
if not play:
|
||||
m = re.search(r'(https?://[^\s\'"]+\.(?:m3u8|mp4)[^\s\'"]*)', html)
|
||||
if m:
|
||||
play = m.group(1)
|
||||
return {
|
||||
'parse': 0 if play else 1,
|
||||
'url' : play or '{}/watch?v={}'.format(self.host, id),
|
||||
'header': self.headers
|
||||
}
|
||||
@@ -536,6 +536,13 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/123AV.py"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "hanime1",
|
||||
"name": "🐬hanime1.py|🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/hanime1.py"
|
||||
},
|
||||
{
|
||||
"key": "Xvideos",
|
||||
"name": "🐬Xvideos.py|🔞",
|
||||
|
||||
Reference in New Issue
Block a user