From 63c7492a8dab0aee253caf5a4c29f2366aaa2daa Mon Sep 17 00:00:00 2001 From: wuyanyun Date: Mon, 1 Jan 2024 02:12:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9F=B3=E4=B9=90=E5=92=8C?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E7=B1=BB=E5=9E=8B=E7=9A=84html=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/DataBase/output_pc.py | 45 ++++++++- app/resources/data/template.html | 161 +++++++++++++++++++++++++++++++ app/ui/contact/export_dialog.py | 5 +- app/util/compress_content.py | 86 +++++++++++++++++ app/util/music.py | 55 +++++++++++ 5 files changed, 347 insertions(+), 5 deletions(-) create mode 100644 app/util/music.py diff --git a/app/DataBase/output_pc.py b/app/DataBase/output_pc.py index c5ea1dc..41a8bc8 100644 --- a/app/DataBase/output_pc.py +++ b/app/DataBase/output_pc.py @@ -20,9 +20,10 @@ from ..DataBase import media_msg_db, hard_link_db, micro_msg_db, msg_db from ..log import logger from ..person import Me from ..util import path -from ..util.compress_content import parser_reply +from ..util.compress_content import parser_reply, music_share from ..util.emoji import get_emoji_url from ..util.file import get_file +from ..util.music import get_music_path from ..util.image import get_image_path, get_image, get_image_abs_path os.makedirs('./data/聊天记录', exist_ok=True) @@ -48,11 +49,23 @@ def makedirs(path): os.makedirs(os.path.join(path, 'voice'), exist_ok=True) os.makedirs(os.path.join(path, 'file'), exist_ok=True) os.makedirs(os.path.join(path, 'avatar'), exist_ok=True) + os.makedirs(os.path.join(path, 'music'), exist_ok=True) + os.makedirs(os.path.join(path, 'icon'), exist_ok=True) file = './app/resources/data/file.png' if not os.path.exists(file): resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) file = os.path.join(resource_dir, 'app', 'resources', 'data', 'file.png') - shutil.copy(file, path + '/file/file.png') + shutil.copy(file, path + '/icon/file.png') + play_file = './app/resources/data/play.png' + if not os.path.exists(play_file): + resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) + play_file = os.path.join(resource_dir, 'app', 'resources', 'data', 'play.png') + shutil.copy(play_file, path + '/icon/play.png') + pause_file = './app/resources/data/pause.png' + if not os.path.exists(pause_file): + resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) + pause_file = os.path.join(resource_dir, 'app', 'resources', 'data', 'pause.png') + shutil.copy(pause_file, path + '/icon/pause.png') def escape_js_and_html(input_str): @@ -437,7 +450,7 @@ class ChildThread(QThread): if self.output_type == Output.HTML: link = get_file(bytesExtra, thumb=True, output_path=origin_docx_path + '/file') file_name = '' - file_path = './file/file.png' + file_path = './icon/file.png' if link != "": file_name = os.path.basename(link) link = './file/' + file_name @@ -621,6 +634,30 @@ class ChildThread(QThread): content_cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER return content_cell + def music_share(self, doc, message): + origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" + is_send = message[4] + timestamp = message[5] + content = music_share(message[11]) + music_path = '' + if content.get('audio_url') != '': + music_path = get_music_path(content.get('audio_url'), content.get('title'), + output_path=origin_docx_path + '/music') + if music_path != '': + music_path = f'./music/{os.path.basename(music_path)}' + music_path = music_path.replace('\\', '/') + is_chatroom = 1 if self.contact.is_chatroom else 0 + avatar = self.get_avatar_path(is_send, message) + display_name = self.get_display_name(is_send, message) + + if self.output_type == Output.HTML: + if content.get('is_error') == False: + doc.write( + f'''{{ type:49, text:'{music_path}',is_send:{is_send},avatar_path:'{avatar}',link_url:'{content.get('link_url')}', + timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{display_name}',sub_type:3,title:'{content.get('title')}', + artist:'{content.get('artist')}', website_name:'{content.get('website_name')}'}},''' + ) + def to_csv(self): origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" os.makedirs(origin_docx_path, exist_ok=True) @@ -693,6 +730,8 @@ class ChildThread(QThread): self.refermsg(f, message) elif type_ == 49 and sub_type == 6 and self.message_types.get(4906): self.file(f, message) + elif type_ == 49 and sub_type == 3 and self.message_types.get(4903): + self.music_share(f, message) f.write(html_end) f.close() self.okSignal.emit(1) diff --git a/app/resources/data/template.html b/app/resources/data/template.html index 2ff4368..e311144 100644 --- a/app/resources/data/template.html +++ b/app/resources/data/template.html @@ -209,6 +209,86 @@ audio{ .chat-file a img,.chat-file div img{ width: 100px; } + +.chat-music-audio { + width: 300px; + margin-right: 20px; + display: flex; + flex-direction: column; + padding: 10px; + background-color: #fff; + border-radius: 4px; + cursor: pointer; + height: 100px; + margin-left: 10px; +} + +.chat-music-audio .player-box { + width: 300px; + display: flex; + align-items: center; + cursor: pointer; + height: 80px; +} + +.chat-music-audio .player-original { + border-top: 1px solid #ede3e3; +} + +.chat-music-audio .player-original p { + margin-top: 5px; + color: #888; +} + +.chat-music-audio .player-controls { + display: flex; + align-items: center;; +} + +.chat-music-audio .flex1 { + flex: 1; + justify-content: start; +} + +.chat-music-audio .flex2 { + flex: 2; + justify-content: end; +} + +.chat-music-audio .player-info { + width: 200px; + height: 80px; + white-space: normal; + flex-basis: 200px; +} + +.chat-music-audio .song-title { + font-weight: bold; + overflow-wrap: break-word; +} + +.chat-music-audio .artist { + margin-top: 10px; + color: #888; +} + +.chat-music-audio .play-button { + width: 50px; + height: 50px; + background-color: rgba(0, 0, 0, 0); + border-radius: 50%; + border: none; + outline: none; + cursor: pointer; +} + +.chat-music-audio .play-button.playing { + background: url("./icon/pause.png"); +} + +.chat-music-audio .play-button.paused { + background: url("./icon/play.png"); +} .input-area{ border-top:0.5px solid #e0e0e0; height: 150px; @@ -619,6 +699,53 @@ input { return messageFileTag; } + function messageMusicAudioBox(message) { + const messageMusicAudioTag = document.createElement('div'); + messageMusicAudioTag.className = `chat-music-audio`; + messageMusicAudioTag.dataset.link = message.link_url; + messageMusicAudioTag.onclick = function (event) { + if (!event.target.classList.contains('play-button')) { + window.open(message.link_url, '_blank'); + } + } + if (message.title.length >= 39) { + message.title = message.title.slice(0, 38) + '...' + } + messageMusicAudioTag.innerHTML = `
+
+
${message.title}
+
+
+
+
+

${message.website_name}

+ + ` + if (message.text != '') { + var audio = document.createElement('audio'); + audio.src = message.text; + messageMusicAudioTag.querySelector('.player-controls').append(audio) + }; + var artist = document.createElement('div'); + artist.className = 'artist'; + artist.innerHTML = message.artist + if (message.title.length < 26) { + messageMusicAudioTag.querySelector('.player-info').append(artist) + } + var playButton = document.createElement('button'); + playButton.className = 'play-button paused'; + playButton.onclick = function (event) { + event.stopPropagation(); // 阻止点击播放按钮时触发父级的点击事件 + toggleAudio(event.target); + }; + if (message.is_send) { + messageMusicAudioTag.querySelector('.player-controls').append(playButton) + } else { + messageMusicAudioTag.querySelector('.player-controls').prepend(playButton) + } + return messageMusicAudioTag; + } + // 从数据列表中取出对应范围的元素并添加到容器中 for (let i = startIndex; i < endIndex && i < chatMessages.length; i++) { const message = chatMessages[i]; @@ -721,6 +848,20 @@ input { messageElement.appendChild(message.is_send ? messageContent : avatarTag); messageElement.appendChild(message.is_send ? avatarTag : messageContent); } + if (message.sub_type == 3) { + // displayname 和 file + messageContent.className = `content-wrapper content-wrapper-${side}`; + if (message.is_chatroom && !message.is_send) { + messageContent.appendChild(displayNameBox(message)); + } + messageContent.appendChild(messageMusicAudioBox(message)); + + // 整合 + messageElement.className = `item item-${side}`; + messageElement.appendChild(message.is_send ? messageContent : avatarTag); + messageElement.appendChild(message.is_send ? avatarTag : messageContent); + } + } else if (message.type == 34) { // displayname 和 转的文字 和 audio @@ -844,6 +985,26 @@ input { var modal = document.getElementById("modal"); modal.style.display = "none"; } + + + function toggleAudio(buttonElm) { + var audioPlayer = buttonElm.parentNode; + var audio = audioPlayer.querySelector('audio'); + if (audio == null){ + alert("该音频已失效或无法直接播放,有需要请点击音频链接查看") + }else{ + if (audio.paused) { + audio.play(); + buttonElm.classList.remove('paused'); + buttonElm.classList.add('playing'); + } else { + audio.pause(); + buttonElm.classList.remove('playing'); + buttonElm.classList.add('paused'); + } + } + + } \ No newline at end of file diff --git a/app/ui/contact/export_dialog.py b/app/ui/contact/export_dialog.py index 661c36c..2a89f53 100644 --- a/app/ui/contact/export_dialog.py +++ b/app/ui/contact/export_dialog.py @@ -12,6 +12,7 @@ types = { '语音': 34, '视频': 43, '表情包': 47, + '音乐与音频': 4903, '文件': 4906, '拍一拍等系统消息': 10000 } @@ -33,7 +34,7 @@ class ExportDialog(QDialog): if file_type == 'html': self.export_type = Output.HTML self.export_choices = {"文本": True, "图片": True, "语音": False, "视频": False, "表情包": False, - '文件': True, + '音乐与音频': False,'文件': False, '拍一拍等系统消息': True} # 定义导出的数据类型,默认全部选择 elif file_type == 'csv': self.export_type = Output.CSV @@ -64,7 +65,7 @@ class ExportDialog(QDialog): layout.addWidget(self.time_label) self.notice_label = QLabel(self) self.notice_label.setText( - "注意:导出HTML时选择图片、视频、语音、表情包(特别是表情包)\n会导致大大影响导出速度,请合理选择导出的类型") + "注意:导出HTML时选择图片、视频、语音、文件、音乐与音频、表情包(特别是表情包)\n会导致大大影响导出速度,请合理选择导出的类型") layout.addWidget(self.notice_label) hlayout = QHBoxLayout(self) self.export_button = QPushButton("导出") diff --git a/app/util/compress_content.py b/app/util/compress_content.py index 6f91dc4..8450e81 100644 --- a/app/util/compress_content.py +++ b/app/util/compress_content.py @@ -3,6 +3,12 @@ import xml.etree.ElementTree as ET import lz4.block +import requests +from urllib.parse import urlparse +from bs4 import BeautifulSoup + + + def decompress_CompressContent(data): """ 解压缩Msg:CompressContent内容 @@ -80,3 +86,83 @@ def parser_reply(data: bytes): }, "is_error": True } + + +def music_share(data: bytes): + xml_content = decompress_CompressContent(data) + if not xml_content: + return { + 'type': 3, + 'title': "发生错误", + "is_error": True + } + try: + root = ET.XML(xml_content) + appmsg = root.find('appmsg') + msg_type = int(appmsg.find('type').text) + title = appmsg.find('title').text + if len(title) >= 39: + title = title[:38] + '...' + artist = appmsg.find('des').text + link_url = appmsg.find('url').text # 链接地址 + audio_url = get_audio_url(appmsg.find('dataurl').text) # 播放地址 + website_name = get_website_name(link_url) + return { + 'type': msg_type, + 'title': title, + 'artist': artist, + 'link_url': link_url, + 'audio_url': audio_url, + 'website_name': website_name, + "is_error": False + } + except Exception as e: + print(f"Music Share Error: {e}") + return { + 'type': 3, + 'title': "发生错误", + "is_error": True + } + + +def get_website_name(url): + parsed_url = urlparse(url) + domain = f"{parsed_url.scheme}://{parsed_url.netloc}" + website_name = '' + try: + response = requests.get(domain, allow_redirects=False) + if response.status_code == 200: + soup = BeautifulSoup(response.content, 'html.parser') + website_name = soup.title.string.strip() + elif response.status_code == 302: + domain = response.headers['Location'] + response = requests.get(domain, allow_redirects=False) + soup = BeautifulSoup(response.content, 'html.parser') + website_name = soup.title.string.strip() + else: + response = requests.get(url, allow_redirects=False) + if response.status_code == 200: + soup = BeautifulSoup(response.content, 'html.parser') + website_name = soup.title.string.strip() + index = website_name.find("-") + if index != -1: # 如果找到了 "-" + website_name = website_name[index+1:].strip() + except Exception as e: + print(f"Get Website Info Error: {e}") + return website_name + + +def get_audio_url(url): + path = '' + try: + response = requests.get(url, allow_redirects=False) + # 检查响应状态码 + if response.status_code == 302: + path = response.headers['Location'] + elif response.status_code == 200: + print('音乐文件已失效,url:' + url) + else: + print('音乐文件地址获取失败,url:' + url +',状态码' + str(response.status_code)) + except Exception as e: + print(f"Get Audio Url Error: {e}") + return path diff --git a/app/util/music.py b/app/util/music.py new file mode 100644 index 0000000..6ad9296 --- /dev/null +++ b/app/util/music.py @@ -0,0 +1,55 @@ +import os +import traceback +import shutil + +from app.log import log, logger +from app.util.protocbuf.msg_pb2 import MessageBytesExtra +import requests +from urllib.parse import urlparse, parse_qs +import re + +root_path = './data/music/' +if not os.path.exists('./data'): + os.mkdir('./data') +if not os.path.exists(root_path): + os.mkdir(root_path) + + +class File: + def __init__(self): + self.open_flag = False + + +def get_music_path(url, file_title, output_path=root_path) -> str: + try: + parsed_url = urlparse(url) + if '.' in parsed_url.path: + # 获取扩展名 + file_extension = parsed_url.path.split('.')[-1] + + pattern = r'[\\/:*?"<>|\r\n]+' + file_title = re.sub(pattern, "_", file_title) + file_name = file_title + '.' + file_extension + music_path = os.path.join(output_path, file_name) + if os.path.exists(music_path): + # print('文件' + music_path + '已存在') + return music_path + header = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.40 Safari/537.36 Edg/87.0.664.24' + } + requests.packages.urllib3.disable_warnings() + response = requests.get(url,headers=header,verify=False) + if response.status_code == 200: + with open(music_path, 'wb') as f: + f.write(response.content) + else: + music_path = '' + print("音乐" + file_name + "获取失败:请求地址:" + url) + else: + music_path = '' + print('音乐文件已失效,url:' + url) + return music_path + except Exception as e: + print(f"Get Music Path Error: {e}") + logger.error(traceback.format_exc()) + return ""