PyQt/QFlowLayout/HotPlaylist.py

316 lines
13 KiB
Python
Raw Normal View History

2018-12-24 00:08:46 +08:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
2021-07-13 14:52:26 +08:00
"""
2018-12-24 00:08:46 +08:00
Created on 2018年2月4日
2021-07-13 14:52:26 +08:00
@author: Irony
@site: https://pyqt.site , https://github.com/PyQt5
2018-12-24 00:08:46 +08:00
@email: 892768447@qq.com
@file: TencentMovieHotPlay_Flow
@description:
2021-07-13 14:52:26 +08:00
"""
2018-12-24 00:08:46 +08:00
import os
import sys
import webbrowser
from PyQt5.QtCore import QSize, Qt, QUrl, QTimer, pyqtSignal
2021-07-13 14:52:26 +08:00
from PyQt5.QtGui import QPainter, QFont, QLinearGradient, QGradient, QColor, \
2018-12-24 00:08:46 +08:00
QBrush, QPaintEvent, QPixmap
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtSvg import QSvgWidget
2021-07-13 14:52:26 +08:00
from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QLabel, \
2018-12-24 00:08:46 +08:00
QHBoxLayout, QSpacerItem, QSizePolicy, QScrollArea, QAbstractSlider
from lxml.etree import HTML # @UnresolvedImport
2021-07-13 14:52:26 +08:00
from Lib.flowlayout import FlowLayout # @UnresolvedImport
2018-12-24 00:08:46 +08:00
# offset=0,30,60,90
Url = "http://v.qq.com/x/list/movie?pay=-1&offset={0}"
# 播放量图标
Svg_icon_play_sm = '''<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<path d="M10.83 8.31v.022l-4.08 2.539-.005.003-.048.03-.012-.005c-.073.051-.15.101-.246.101-.217 0-.376-.165-.413-.369l-.027-.011V5.461l.009-.005c0-.009-.009-.014-.009-.022 0-.24.197-.435.44-.435.096 0 .174.049.247.101l.031-.017 4.129 2.569v.016a.42.42 0 0 1 .153.317.418.418 0 0 1-.169.325zm3.493 2.604a.986.986 0 0 1-.948.742 1 1 0 0 1-1-1 .98.98 0 0 1 .094-.412l-.019-.01C12.79 9.559 13 8.807 13 8a5 5 0 1 0-5 5c.766 0 1.484-.186 2.133-.494l.013.03a.975.975 0 0 1 .417-.097 1 1 0 0 1 1 1 .987.987 0 0 1-.77.954A6.936 6.936 0 0 1 8 14.999a7 7 0 1 1 7-7c0 1.048-.261 2.024-.677 2.915z" fill="#999999"></path>
</svg>
'''.encode()
Svg_icon_loading = '''<svg width="100%" height="100%" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient x1="8.042%" y1="0%" x2="65.682%" y2="23.865%" id="a">
<stop stop-color="#03a9f4" stop-opacity="0" offset="0%"/>
<stop stop-color="#03a9f4" stop-opacity=".631" offset="63.146%"/>
<stop stop-color="#03a9f4" offset="100%"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)">
<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke="url(#a)" stroke-width="2">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.5s"
repeatCount="indefinite" />
</path>
<circle fill="#03a9f4" cx="36" cy="18" r="4">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.5s"
repeatCount="indefinite" />
</circle>
</g>
</g>
</svg>'''.encode()
# 主演
Actor = '''<a href="{href}" target="_blank" title="{title}" style="text-decoration: none;font-size: 12px;color: #999999;">{title}</a>&nbsp;'''
class CoverLabel(QLabel):
def __init__(self, cover_path, cover_title, video_url, *args, **kwargs):
super(CoverLabel, self).__init__(*args, **kwargs)
2021-07-13 14:52:26 +08:00
# super(CoverLabel, self).__init__(
# '<html><head/><body><img src="{0}"/></body></html>'.format(os.path.abspath(cover_path)), *args, **kwargs)
2018-12-24 00:08:46 +08:00
self.setCursor(Qt.PointingHandCursor)
self.setScaledContents(True)
self.setMinimumSize(220, 308)
self.setMaximumSize(220, 308)
self.cover_path = cover_path
self.cover_title = cover_title
self.video_url = video_url
self.setPixmap(QPixmap(cover_path))
def setCoverPath(self, path):
self.cover_path = path
def mouseReleaseEvent(self, event):
super(CoverLabel, self).mouseReleaseEvent(event)
webbrowser.open_new_tab(self.video_url)
def paintEvent(self, event):
super(CoverLabel, self).paintEvent(event)
if hasattr(self, "cover_title") and self.cover_title != "":
# 底部绘制文字
painter = QPainter(self)
rect = self.rect()
# 粗略字体高度
painter.save()
fheight = self.fontMetrics().height()
# 底部矩形框背景渐变颜色
bottomRectColor = QLinearGradient(
rect.width() / 2, rect.height() - 24 - fheight,
rect.width() / 2, rect.height())
bottomRectColor.setSpread(QGradient.PadSpread)
bottomRectColor.setColorAt(0, QColor(255, 255, 255, 70))
bottomRectColor.setColorAt(1, QColor(0, 0, 0, 50))
# 画半透明渐变矩形框
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(bottomRectColor))
painter.drawRect(rect.x(), rect.height() - 24 -
fheight, rect.width(), 24 + fheight)
painter.restore()
# 距离底部一定高度画文字
font = self.font() or QFont()
font.setPointSize(8)
painter.setFont(font)
painter.setPen(Qt.white)
rect.setHeight(rect.height() - 12) # 底部减去一定高度
painter.drawText(rect, Qt.AlignHCenter |
Qt.AlignBottom, self.cover_title)
class ItemWidget(QWidget):
def __init__(self, cover_path, figure_info, figure_title,
figure_score, figure_desc, figure_count, video_url, cover_url, img_path, *args, **kwargs):
super(ItemWidget, self).__init__(*args, **kwargs)
self.setMaximumSize(220, 420)
self.setMaximumSize(220, 420)
self.img_path = img_path
self.cover_url = cover_url
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 20, 10, 0)
# 图片label
self.clabel = CoverLabel(cover_path, figure_info, video_url, self)
layout.addWidget(self.clabel)
# 片名和分数
flayout = QHBoxLayout()
flayout.addWidget(QLabel(figure_title, self))
flayout.addItem(QSpacerItem(
20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
flayout.addWidget(QLabel(figure_score, self, styleSheet="color: red;"))
layout.addLayout(flayout)
# 主演
layout.addWidget(
QLabel(figure_desc, self, styleSheet="color: #999999;", openExternalLinks=True))
# 播放量
blayout = QHBoxLayout()
count_icon = QSvgWidget(self)
count_icon.setMaximumSize(16, 16)
count_icon.load(Svg_icon_play_sm)
blayout.addWidget(count_icon)
blayout.addWidget(
QLabel(figure_count, self, styleSheet="color: #999999;"))
layout.addLayout(blayout)
def setCover(self, path):
self.clabel.setCoverPath(path)
self.clabel.setPixmap(QPixmap(path))
2021-07-13 14:52:26 +08:00
# self.clabel.setText('<img src="{0}"/>'.format(os.path.abspath(path)))
2018-12-24 00:08:46 +08:00
def sizeHint(self):
# 每个item控件的大小
return QSize(220, 420)
def event(self, event):
if isinstance(event, QPaintEvent):
if event.rect().height() > 20 and hasattr(self, "clabel"):
if self.clabel.cover_path.find("pic_v.png") > -1: # 封面未加载
# print("start download img:", self.cover_url)
req = QNetworkRequest(QUrl(self.cover_url))
# 设置两个自定义属性方便后期reply中处理
req.setAttribute(QNetworkRequest.User + 1, self)
req.setAttribute(QNetworkRequest.User + 2, self.img_path)
self.parentWidget()._manager.get(req) # 调用父窗口中的下载器下载
return super(ItemWidget, self).event(event)
class GridWidget(QWidget):
Page = 0
loadStarted = pyqtSignal(bool)
def __init__(self, *args, **kwargs):
super(GridWidget, self).__init__(*args, **kwargs)
self._layout = FlowLayout(self) # 使用自定义流式布局
# 异步网络下载管理器
self._manager = QNetworkAccessManager(self)
self._manager.finished.connect(self.onFinished)
def load(self):
if self.Page == -1:
return
self.loadStarted.emit(True)
# 延迟一秒后调用目的在于显示进度条
QTimer.singleShot(1000, self._load)
def _load(self):
print("load url:", Url.format(self.Page * 30))
url = QUrl(Url.format(self.Page * 30))
self._manager.get(QNetworkRequest(url))
def onFinished(self, reply):
# 请求完成后会调用该函数
req = reply.request() # 获取请求
iwidget = req.attribute(QNetworkRequest.User + 1, None)
path = req.attribute(QNetworkRequest.User + 2, None)
html = reply.readAll().data()
reply.deleteLater()
del reply
if iwidget and path and html:
# 这里是图片下载完毕
open(path, "wb").write(html)
iwidget.setCover(path)
return
# 解析网页
self._parseHtml(html)
self.loadStarted.emit(False)
def _parseHtml(self, html):
# encoding = chardet.detect(html) or {}
# html = html.decode(encoding.get("encoding","utf-8"))
html = HTML(html)
# 查找所有的li list_item
lis = html.xpath("//li[@class='list_item']")
if not lis:
self.Page = -1 # 后面没有页面了
return
self.Page += 1
self._makeItem(lis)
def _makeItem(self, lis):
for li in lis:
a = li.find("a")
video_url = a.get("href") # 视频播放地址
img = a.find("img")
cover_url = "http:" + img.get("r-lazyload") # 封面图片
figure_title = img.get("alt") # 电影名
figure_info = a.find("div/span")
figure_info = "" if figure_info is None else figure_info.text # 影片信息
figure_score = "".join(li.xpath(".//em/text()")) # 评分
# 主演
figure_desc = "<span style=\"font-size: 12px;\">主演:</span>" + \
2021-07-13 14:52:26 +08:00
"".join([Actor.format(**dict(fd.items()))
for fd in li.xpath(".//div[@class='figure_desc']/a")])
2018-12-24 00:08:46 +08:00
# 播放数
figure_count = (
2021-07-13 14:52:26 +08:00
li.xpath(".//div[@class='figure_count']/span/text()") or [""])[0]
2018-12-24 00:08:46 +08:00
path = "cache/{0}.jpg".format(
os.path.splitext(os.path.basename(video_url))[0])
2018-12-25 00:48:54 +08:00
cover_path = "Data/pic_v.png"
2018-12-24 00:08:46 +08:00
if os.path.isfile(path):
cover_path = path
iwidget = ItemWidget(cover_path, figure_info, figure_title,
figure_score, figure_desc, figure_count, video_url, cover_url, path, self)
self._layout.addWidget(iwidget)
class Window(QScrollArea):
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
self.resize(800, 600)
self.setFrameShape(self.NoFrame)
self.setWidgetResizable(True)
self.setAlignment(Qt.AlignCenter)
self._loadStart = False
# 网格窗口
self._widget = GridWidget(self)
self._widget.loadStarted.connect(self.setLoadStarted)
self.setWidget(self._widget)
# 连接竖着的滚动条滚动事件
self.verticalScrollBar().actionTriggered.connect(self.onActionTriggered)
# 进度条
self.loadWidget = QSvgWidget(
self, minimumHeight=120, minimumWidth=120, visible=False)
self.loadWidget.load(Svg_icon_loading)
def setLoadStarted(self, started):
self._loadStart = started
self.loadWidget.setVisible(started)
def onActionTriggered(self, action):
# 这里要判断action=QAbstractSlider.SliderMove可以避免窗口大小改变的问题
# 同时防止多次加载同一个url
if action != QAbstractSlider.SliderMove or self._loadStart:
return
# 使用sliderPosition获取值可以同时满足鼠标滑动和拖动判断
if self.verticalScrollBar().sliderPosition() == self.verticalScrollBar().maximum():
# 可以下一页了
self._widget.load()
def resizeEvent(self, event):
super(Window, self).resizeEvent(event)
self.loadWidget.setGeometry(
int((self.width() - self.loadWidget.minimumWidth()) / 2),
int((self.height() - self.loadWidget.minimumHeight()) / 2),
self.loadWidget.minimumWidth(),
self.loadWidget.minimumHeight()
)
if __name__ == "__main__":
os.makedirs("cache", exist_ok=True)
app = QApplication(sys.argv)
w = Window()
w.show()
w._widget.load()
sys.exit(app.exec_())