7 Commits
2.1.1 ... 2.1.2

Author SHA1 Message Date
putyy
240bae9be9 修改md 2024-10-15 13:42:52 +08:00
putyy
46b1592a7f 更新版本号 2024-10-15 13:37:52 +08:00
putyy
1cae714fd2 Merge pull request #90 from putyy/dev
完善资源类型、优化资源类型选择、优化table固定表头等
2024-10-15 11:19:48 +08:00
putyy
5e63624955 完善资源类型、优化资源类型选择、优化table固定表头等 2024-10-15 11:19:17 +08:00
putyy
7584156262 Update README.md 2024-10-10 09:05:09 +08:00
putyy
d23c09748b 调整打包 2024-09-04 09:20:20 +08:00
putyy
936adca3f2 更新执行文件 2024-09-03 16:43:24 +08:00
15 changed files with 183 additions and 206 deletions

View File

@@ -1,8 +1,9 @@
## res-downloader(爱享素材下载器) 【[点击加入群聊](https://qm.qq.com/q/W8mVeZideE)】
## res-downloader
### 爱享素材下载器【[加入群聊](https://qm.qq.com/q/mfDMSpCxQ4)】
🎯 基于 [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue.git)
📦 操作简单、可获取不同类型的资源
🖥️ 支持Win10、Win11、Mac
🌐 支持视频、音频、图片、m3u8网络资源下载
🖥️ 支持Win10、Win11、Mac、Linux
🌐 支持视频、音频、图片、m3u8、直播流等常见网络资源拦截
💪 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载
👼 支持设置代理以获取特殊网络下的资源
@@ -21,7 +22,7 @@
![](public/show.webp)
## 常见问题
下载慢、大视频下载失败
下载慢、大视频下载失败(最新版本以内置aria2下载器)
> 推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密(视频号)” 按钮
>> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
@@ -39,7 +40,8 @@ Win7无法使用
>> MAC: /Users/你的用户名称/.res-downloader@putyy/res-downloader-installed.lock
>> Win: C:\Users\Admin\.res-downloader@putyy/res-downloader-installed.lock
其他问题请留言 https://github.com/putyy/res-downloader/issues
其他问题
[github](https://github.com/putyy/res-downloader/issues) 、 [爱享论坛](https://s.gowas.cn/d/4089)
## 二次开发
> ps 打包慢的问题可以参考 https://www.putyy.com/articles/87
@@ -59,5 +61,8 @@ yarn run build --universal --mac
yarn run build --win
```
## 实现&初衷
通过代理网络抓包拦截响应,筛选出有用的资源, 同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的只不过这些软件需要手动进行筛选对于小白用户上手还是有点难度本软件对部分资源做了特殊处理更适合大众用户所以就有了本项目。
## 免责声明
本软件用于学习研究使用,若因使用本软件造成的一切法律责任均与本人无关!

1
components.d.ts vendored
View File

@@ -20,7 +20,6 @@ declare module 'vue' {
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
Footer: typeof import('./src/components/layout/Footer.vue')['default']

View File

@@ -10,8 +10,7 @@
},
"files": [
"dist-electron",
"dist",
"electron/res/**/*"
"dist"
],
"mac": {
"icon": "electron/res/icon/mac.icns",

View File

@@ -144,6 +144,8 @@ function createPreviewWindow(parent: BrowserWindow) {
parent: parent,
width: 600,
height: 400,
minWidth: 600,
minHeight: 400,
show: false,
// paintWhenInitiallyHidden: false,
webPreferences: {

View File

@@ -1,11 +1,12 @@
import {ipcMain, dialog, BrowserWindow, app, shell} from 'electron'
import {startServer} from './proxyServer'
import {installCert, checkCertInstalled} from './cert'
import {decodeWxFile, suffix, getCurrentDateTimeFormatted} from './utils'
import {decodeWxFile, typeSuffix, getCurrentDateTimeFormatted} from './utils'
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
import {Aria2RPC} from './aria2Rpc'
import fs from "fs"
import urlTool from "url";
let win: BrowserWindow
let previewWin: BrowserWindow
@@ -71,7 +72,16 @@ export default function initIPC() {
resolve(false);
});
}
if (quality !== "-1" && data.decode_key && data.file_format) {
if(quality === "0" && data.decode_key){
const urlInfo = urlTool.parse(down_url, true);
console.log('urlInfo', urlInfo)
if (urlInfo.query["token"] && urlInfo.query["encfilekey"]) {
down_url = urlInfo.protocol + "//" + urlInfo.hostname + urlInfo.pathname.replace("251/20302", "251/20304") +
"?encfilekey=" + urlInfo.query["encfilekey"] +
"&token=" + urlInfo.query["token"]
console.log("down_url:", down_url)
}
} else if (quality !== "-1" && data.decode_key && data.file_format) {
const format = data.file_format.split('#');
const qualityMap = [
format[0],
@@ -81,7 +91,7 @@ export default function initIPC() {
down_url += "&X-snsvideoflag=" + qualityMap[quality];
}
let fileName = data?.description ? data.description.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '') : hexMD5(down_url);
fileName = fileName + "_" + getCurrentDateTimeFormatted() + suffix(data.type)
fileName = fileName + "_" + getCurrentDateTimeFormatted() + typeSuffix(data.type)[1]
let save_path_file = `${save_path}/${fileName}`
if (process.platform === 'win32') {
save_path_file = `${save_path}\\${fileName}`

View File

@@ -3,7 +3,7 @@ import log from 'electron-log'
import CONFIG from './const'
import {setProxy} from './setProxy'
import * as urlTool from "url"
import {toSize} from "./utils"
import {toSize, typeSuffix} from "./utils"
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
import pkg from '../../package.json'
@@ -146,87 +146,24 @@ export async function startServer({win, upstreamProxy, setProxyErrorCallback = f
async (req, res) => {
try {
// 拦截响应
const ctype = res?._data?.headers?.['content-type']
const url_sign: string = hexMD5(req.fullUrl())
const res_url = req.fullUrl()
const urlInfo = urlTool.parse(res_url, true)
switch (ctype) {
case "video/mp4":
case "video/webm":
case "video/ogg":
case "video/x-msvideo":
case "video/mpeg":
case "video/quicktime":
case "video/x-ms-wmv":
case "video/x-flv":
case "video/3gpp":
case "video/x-matroska":
if (global.videoList.hasOwnProperty(url_sign) === false) {
global.videoList[url_sign] = res_url
win.webContents.send('on_get_queue', Object.assign({}, resObject, {
url: res_url,
url_sign: url_sign,
platform: urlInfo.hostname,
size: toSize(res?._data?.headers?.['content-length'] ?? 0),
type: ctype,
type_str: 'video',
}))
}
break;
case "image/png":
case "image/webp":
case "image/jpeg":
case "image/jpg":
case "image/svg+xml":
case "image/gif":
case "image/avif":
case "image/bmp":
case "image/tiff":
case "image/x-icon":
case "image/heic":
case "image/vnd.adobe.photoshop":
const contentType = res?._data?.headers?.['content-type']
const [resType, suffix] = typeSuffix(contentType)
if (resType) {
const url_sign: string = hexMD5(req.fullUrl())
const res_url = req.fullUrl()
const urlInfo = urlTool.parse(res_url, true)
const contentLength = res?._data?.headers?.['content-length']
if (global.videoList.hasOwnProperty(url_sign) === false) {
global.videoList[url_sign] = res_url
win.webContents.send('on_get_queue', Object.assign({}, resObject, {
url: res_url,
url_sign: url_sign,
platform: urlInfo.hostname,
size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0,
type: ctype,
type_str: 'image',
size: toSize(contentLength ? contentLength : 0),
type: contentType,
type_str: resType,
}))
break
case "audio/mpeg":
case "audio/wav":
case "audio/aiff":
case "audio/x-aiff":
case "audio/aac":
case "audio/ogg":
case "audio/flac":
case "audio/midi":
case "audio/x-midi":
case "audio/x-ms-wma":
case "audio/opus":
case "audio/webm":
case "audio/mp4":
win.webContents.send('on_get_queue', Object.assign({}, resObject, {
url: res_url,
url_sign: url_sign,
platform: urlInfo.hostname,
size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0,
type: ctype,
type_str: 'audio',
}))
break
case "application/vnd.apple.mpegurl":
case "application/x-mpegURL":
win.webContents.send('on_get_queue', Object.assign({}, resObject, {
url: res_url,
url_sign: url_sign,
platform: urlInfo.hostname,
size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0,
type: ctype,
type_str: 'm3u8',
}))
break
}
}
} catch (e) {
log.log(e.toString())

View File

@@ -1,8 +1,9 @@
import fs from 'fs'
import {Transform } from 'stream'
import {Transform} from 'stream'
import {getDecryptionArray} from '../wxjs/decrypt.js'
const axios = require('axios')
function xorTransform(decryptionArray) {
let processedBytes = 0;
return new Transform({
@@ -32,7 +33,7 @@ function downloadFile(url, decodeKey, fullFileName, progressCallback) {
},
}
if (url.includes("douyin")){
if (url.includes("douyin")) {
config.headers['Referer'] = url
}
@@ -57,7 +58,7 @@ function downloadFile(url, decodeKey, fullFileName, progressCallback) {
});
}),
);
}else{
} else {
data.pipe(
fs.createWriteStream(fullFileName).on('finish', () => {
resolve({
@@ -97,7 +98,7 @@ function toSize(size: number) {
return size + 'b'
}
function suffix(type: string) {
function typeSuffix(type: string) {
switch (type) {
case "video/mp4":
case "video/webm":
@@ -106,23 +107,25 @@ function suffix(type: string) {
case "video/mpeg":
case "video/quicktime":
case "video/x-ms-wmv":
case "video/x-flv":
case "video/3gpp":
case "video/x-matroska":
return ".mp4";
return ["video", ".mp4"];
case "audio/video":
case "video/x-flv":
return ["live", ".mp4"];
case "image/png":
case "image/webp":
case "image/jpeg":
case "image/jpg":
case "image/svg+xml":
case "image/gif":
case "image/avif":
case "image/bmp":
case "image/tiff":
case "image/x-icon":
case "image/heic":
case "image/x-icon":
case "image/svg+xml":
case "image/vnd.adobe.photoshop":
return ".png";
return ["image", ".png"];
case "audio/mpeg":
case "audio/wav":
case "audio/aiff":
@@ -136,12 +139,23 @@ function suffix(type: string) {
case "audio/opus":
case "audio/webm":
case "audio/mp4":
return ".mp3";
return ["audio", ".mp3"];
case "application/vnd.apple.mpegurl":
case "application/x-mpegURL":
return ".m3u8";
return ["m3u8", ".m3u8"];
case "application/pdf":
return ["pdf", ".pdf"];
case "application/vnd.ms-powerpoint":
case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
return ["ppt", ".ppt"];
case "application/vnd.ms-excel":
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return ["xls", ".xls"];
case "application/msword":
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return ["doc", ".doc"];
}
return ""
return ["", ""]
}
function getCurrentDateTimeFormatted() {
@@ -157,4 +171,4 @@ function getCurrentDateTimeFormatted() {
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
export {downloadFile, toSize, decodeWxFile, suffix, getCurrentDateTimeFormatted}
export {downloadFile, toSize, decodeWxFile, typeSuffix, getCurrentDateTimeFormatted}

0
electron/res/darwin/aria2/arm64/aria2c Normal file → Executable file
View File

0
electron/res/darwin/aria2/x64/aria2c Normal file → Executable file
View File

0
electron/res/win/aria2/aria2c.exe Normal file → Executable file
View File

View File

@@ -1,6 +1,6 @@
{
"name": "res-downloader",
"version": "2.1.1",
"version": "2.1.2",
"main": "dist-electron/main/index.js",
"description": "res-downloader(爱享素材下载器)支持视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐、qq短视频等",
"homepage": "https://github.com/putyy/res-downloader",
@@ -58,8 +58,6 @@
},
"dependencies": {
"axios": "^1.5.0",
"electron-store": "^8.1.0",
"getmac": "^5.20.0",
"hoxy": "^3.3.1",
"tunnel-agent": "^0.6.0"
}

View File

@@ -2,53 +2,17 @@
import {ipcRenderer} from 'electron'
import pkg from '../../../package.json'
const v = pkg.version
const jump = (scene: number)=>{
switch (scene) {
case 1:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian"
})
break;
case 2:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://s.gowas.cn"
})
break;
case 3:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://i.gowas.cn"
})
break;
case 4:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian"
})
break;
case 5:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://www.ais.do/ivi/rr2GaZ"
})
break;
case 6:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://github.com/putyy/res-downloader"
})
break;
}
const jump = (url: string)=>{
ipcRenderer.invoke('invoke_open_default_browser', {
url: url
})
}
</script>
<template lang="pug">
div.line
a.item 当前版本: {{v}}
a.item 站长邮箱: gowas.work@gmail.com
a.item(@click="jump(1)") 获取更新
a.item(@click="jump(4)") 问题反馈
div.line
a.item 推荐:
a.item(@click="jump(5)") Ai助手(免费)
a.item(@click="jump(2)") 网盘资源
a.item(@click="jump(3)") 图片无损压缩
a.item(@click="jump(6)") 软件源码
a.item(@click="jump('https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian')") 获取更新&问题反馈
a.item(@click="jump('https://github.com/putyy/res-downloader')") 软件源码
</template>
<style lang="less" scoped>

View File

@@ -16,8 +16,8 @@ el-container
keep-alive(v-if="route.meta.keepAlive")
component(:is="Component")
component(v-else :is="Component")
el-footer
Footer
el-footer
Footer
</template>
<style lang="less" scoped>
@@ -27,4 +27,9 @@ el-container
.el-main {
text-align: center;
}
.el-footer{
margin: unset !important;
padding: unset !important;
height: auto !important;
}
</style>

View File

@@ -30,6 +30,7 @@ const str = "使用方法\n" +
" 2. 软件首页选择要获取的资源类型(默认选中的视频)\n" +
" 3. 打开要捕获的源, 如:视频号、网页、小程序等等\n" +
" 4. 返回软件首页即可看到要下载的资源\n" +
" 5. 直播流复制的链接如何使用可以使用obs或者ffmpeg命令\n" +
"常见问题\n" +
" 1. 无法拦截获取\n" +
" 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899\n" +
@@ -56,8 +57,7 @@ div.about
el-button(@click="jump(3)") 获取更新
div 4. 问题反馈 &nbsp;
el-button(@click="jump(4)") 点击前往
div.more
pre {{str}}
div.more {{str}}
</template>
@@ -74,7 +74,9 @@ div.about
white-space: pre-wrap;
}
.more{
width: 100%; /* 设置容器宽度 */
white-space: pre-wrap;
overflow-wrap: break-word; /* 允许在单词边界内换行 */
}
}
</style>

View File

@@ -20,24 +20,51 @@ interface resData {
const tableData = ref<resData[]>([])
const resType = ref({
video: true,
audio: true,
image: false,
m3u8: false
})
const isInitApp = ref(false)
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
const multipleSelection = ref<resData[]>([])
const loading = ref()
const resType = ref(["all"])
const typeOptions = ref([
{
value: "all",
label: "全部",
},
{
value: "image",
label: "图片",
}, {
value: "audio",
label: "音频"
}, {
value: "video",
label: "视频"
}, {
value: "m3u8",
label: "m3u8"
}, {
value: "live",
label: "直播流"
}, {
value: "xls",
label: "文档"
}, {
value: "doc",
label: "doc"
}, {
value: "pdf",
label: "pdf"
}
])
const tableHeight = ref(400)
onMounted(() => {
let resTypeCache = localStorageCache.get("res-type")
let resTypeCache = localStorageCache.get("res-type-arr")
if (resTypeCache) {
resType.value = resTypeCache
resType.value = resTypeCache.split(",")
}
let tableDataCache = localStorageCache.get("res-table-data")
@@ -47,7 +74,7 @@ onMounted(() => {
ipcRenderer.on('on_get_queue', (res, data) => {
// @ts-ignore
if (resType.value.hasOwnProperty(data.type_str) && resType.value[data.type_str]) {
if (resType.value.includes("all") || resType.value.includes(data.type_str)) {
tableData.value.push(data)
localStorageCache.set("res-table-data", tableData.value, -1)
}
@@ -79,6 +106,8 @@ onMounted(() => {
})
loading.value.close()
})
window.addEventListener("resize", handleResize);
handleResize()
})
onUnmounted(() => {
@@ -92,9 +121,14 @@ onUnmounted(() => {
})
watch(resType, (res, res1) => {
localStorageCache.set("res-type", resType.value, -1)
localStorageCache.set("res-type-arr", resType.value.join(","), -1)
}, {deep: true})
const handleResize = () => {
const height = document.documentElement.clientHeight || window.innerHeight;
tableHeight.value = height - 115
}
const handleSelectionChange = (val: resData[]) => {
multipleSelection.value = val
}
@@ -270,55 +304,63 @@ const handleInitApp = () => {
<template lang="pug">
el-container.container
el-header
el-row
div
el-button(type="primary" @click="handleBatchDown") 批量下载
el-button(v-if="isInitApp" @click="handleInitApp")
el-icon
Promotion
p 安装检测(如果看到此按钮说明软件安装未完成则需要手动点击此按钮)
el-button(@click="handleClear")
el-icon
Delete
p 清空列表
el-button(@click="resType.video=!resType.video" :type="resType.video ? 'primary' : 'info'" ) 视频
el-button(@click="resType.audio=!resType.audio" :type="resType.audio ? 'primary' : 'info'" ) 音频
el-button(@click="resType.image=!resType.image" :type="resType.image ? 'primary' : 'info'" ) 图片
el-button(@click="resType.m3u8=!resType.m3u8" :type="resType.m3u8 ? 'primary' : 'info'" ) m3u8
a(style="color: red") &nbsp;&nbsp;&nbsp;点击左边选项选择需要拦截的资源类型
el-main
el-table(ref="multipleTableRef" @selection-change="handleSelectionChange" :data="tableData" max-height="100%" stripe)
el-table-column(type="selection")
el-table-column(label="预览" show-overflow-tooltip width="150px")
template(#default="scope")
div.show_res
video.video(v-if="scope.row.type_str === 'video'" :src="scope.row.url" controls preload="none")
img.img(v-if="scope.row.type_str === 'image'" :src="scope.row.url" crossorigin="anonymous")
audio.audio(v-if="scope.row.type_str === 'audio'" controls preload="none")
source(:src="scope.row.url" :type="scope.row.type")
div {{scope.row.description}}
el-table-column(prop="type_str" label="类型" show-overflow-tooltip)
el-table-column(prop="platform" label="主机地址")
el-table-column(prop="size" label="资源大小")
el-table-column(prop="save_path" label="保存目录")
el-table-column(prop="progress_bar" label="下载进度")
el-table-column(label="操作" width="135px" )
template(#default="scope")
div.actions
template(v-if="scope.row.type_str !== 'm3u8'" )
el-button(link type="primary" @click="handleDown(scope.$index, scope.row)") {{scope.row.decode_key || scope.row.decryptor_array ? "解密下载(视频号)" : "下载"}}
el-button(v-if="scope.row.decode_key || scope.row.decryptor_array" link type="primary" @click="decodeWxFile(scope.$index)") 视频解密(视频号)
el-button(link type="primary" @click="handlePreview(scope.$index, scope.row)") 窗口预览
el-button(link type="primary" @click="handleCopy(scope.row.url)") 复制链接
el-button(link type="primary" @click="handleDel(scope.$index)") 删除
el-button(v-if="scope.row.save_path" link type="primary" @click="openFileDir(scope.$index)") 打开文件目录
el-header(style="display:flex;align-items: center")
el-button(type="primary" @click="handleBatchDown") 批量下载
el-button(v-if="isInitApp" @click="handleInitApp")
el-icon
Promotion
p 安装检测(如果看到此按钮说明软件安装未完成则需要手动点击此按钮)
el-button(@click="handleClear")
el-icon
Delete
p 清空列表
el-select(
v-model="resType"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
placeholder="资源拦截类型"
style="width: auto;min-width:130px"
)
el-option(v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value")
el-table(ref="multipleTableRef" @selection-change="handleSelectionChange" :data="tableData" :height="tableHeight" max-height="100%" stripe)
el-table-column(type="selection")
el-table-column(label="预览" show-overflow-tooltip width="150px")
template(#default="scope")
div.show_res
video.video(v-if="scope.row.type_str === 'video'" :src="scope.row.url" controls preload="none")
img.img(v-if="scope.row.type_str === 'image'" :src="scope.row.url" crossorigin="anonymous")
audio.audio(v-if="scope.row.type_str === 'audio'" controls preload="none")
source(:src="scope.row.url" :type="scope.row.type")
div(v-if="scope.row.type_str !== 'video' && scope.row.type_str !== 'image' && scope.row.type_str !== 'audio'") {{scope.row.type_str}}类型无法预览
div {{scope.row.description}}
el-table-column(prop="type_str" label="类型" show-overflow-tooltip)
el-table-column(prop="platform" label="主机地址")
el-table-column(prop="size" label="资源大小")
el-table-column(prop="save_path" label="保存目录" width="135px" :show-overflow-tooltip="true")
el-table-column(prop="progress_bar" label="下载进度")
el-table-column(label="操作" width="135px" )
template(#default="scope")
div.actions
template(v-if="scope.row.type_str !== 'm3u8' && scope.row.type_str !== 'live'" )
el-button(link type="primary" @click="handleDown(scope.$index, scope.row)") {{scope.row.decode_key || scope.row.decryptor_array ? "解密下载(视频号)" : "下载"}}
el-button(v-if="scope.row.decode_key || scope.row.decryptor_array" link type="primary" @click="decodeWxFile(scope.$index)") 视频解密(视频号)
el-button(link type="primary" @click="handlePreview(scope.$index, scope.row)") 窗口预览
el-button(link type="primary" @click="handleCopy(scope.row.url)") 复制链接
el-button(link type="primary" @click="handleDel(scope.$index)") 删除
el-button(v-if="scope.row.save_path" link type="primary" @click="openFileDir(scope.$index)") 打开文件目录
</template>
<style scoped lang="less">
.container {
padding: 0.5rem;
.el-table{
padding-top: 1rem;
}
.el-button {
margin: 0.1rem;
}