// ==UserScript==
// @name Javdb_Emby助手
// @version 1.0.1
// @author weiShao
// @description Javdb_Emby助手_weiShao
// @license MIT
// @icon https://www.javdb.com/favicon.ico
// @match https://*.javdb.com/*
// @match *://*.javdb.com/*
// @connect jable.tv
// @connect missav.com
// @connect javhhh.com
// @connect netflav.com
// @connect avgle.com
// @connect bestjavporn.com
// @connect jav.guru
// @connect javmost.cx
// @connect hpjav.tv
// @connect av01.tv
// @connect javbus.com
// @connect javmenu.com
// @connect javfc2.net
// @connect paipancon.com
// @connect ggjav.com
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @namespace https://greasyfork.org/users/434740
// ==/UserScript==
;(function () {
'use strict'
/* globals jQuery, $, waitForKeyElements */
/**
* 加载动画
*/
const LoadingGif = {
/**
* 加载动画的元素
*/
element: null,
/**
* 启动加载动画
*/
start: function () {
if (!this.element) {
this.element = document.createElement('img')
this.element.src =
'https://upload.wikimedia.org/wikipedia/commons/3/3a/Gray_circles_rotate.gif'
this.element.alt = '读取文件夹中...'
Object.assign(this.element.style, {
position: 'fixed',
bottom: '0',
left: '50px',
zIndex: '1000',
width: '40px',
height: '40px',
padding: '5px'
})
document.body.appendChild(this.element)
}
},
/**
* 停止加载动画
*/
stop: function () {
if (this.element) {
document.body.removeChild(this.element)
this.element = null
}
}
}
/**
* 本地文件夹处理函数
*/
const LocalFolderHandler = (function () {
class LocalFolderHandlerClass {
constructor() {
this.nfoFileNamesSet = new Set()
this.initButton()
}
/**
* 创建一个按钮元素并添加到页面中
*/
initButton() {
const button = this.createButtonElement()
button.addEventListener('click', this.handleButtonClick.bind(this))
document.body.appendChild(button)
}
/**
* 创建一个按钮元素
* @returns {HTMLButtonElement}
*/
createButtonElement() {
const button = document.createElement('button')
button.innerHTML = '仓'
Object.assign(button.style, {
color: '#fff',
backgroundColor: '#FF8400',
borderColor: '#FF8400',
borderRadius: '5px',
position: 'fixed',
bottom: '2px',
left: '2px',
zIndex: '1000',
padding: '5px 10px',
cursor: 'pointer',
fontSize: '16px',
fontWeight: 'bold'
})
return button
}
/**
* 按钮点击事件处理函数
*/
async handleButtonClick() {
this.nfoFileNamesSet.clear()
const directoryHandle = await window.showDirectoryPicker()
console.log(
'%c Line:90 🍖 directoryHandle',
'color:#42b983',
directoryHandle.name
)
if (!directoryHandle) {
alert('获取本地信息失败')
return
}
const startTime = Date.now()
LoadingGif.start()
for await (const fileData of this.getFiles(directoryHandle, [
directoryHandle.name
])) {
const file = await fileData.fileHandle.getFile()
const videoFullName = await this.findVideoFileName(
fileData.parentDirectoryHandle
)
const item = {
originalFileName: file.name.substring(
0,
file.name.length - '.nfo'.length
),
transformedName: this.processFileName(file.name),
videoFullName: videoFullName,
hierarchicalStructure: [...fileData.folderNames, videoFullName]
}
this.nfoFileNamesSet.add(item)
}
const str = JSON.stringify(Array.from(this.nfoFileNamesSet))
localStorage.setItem('nfoFiles', str)
LoadingGif.stop()
const endTime = Date.now()
const time = ((endTime - startTime) / 1000).toFixed(2)
alert(
`读取文件夹: '${directoryHandle.name}' 成功,耗时 ${time} 秒, 共读取 ${this.nfoFileNamesSet.size} 个视频。`
)
onBeforeMount()
}
/**
* 递归获取目录下的所有文件
* @param {FileSystemDirectoryHandle} directoryHandle - 当前目录句柄
* @param {string[]} folderNames - 目录名数组
* @returns {AsyncGenerator}
*/
async *getFiles(directoryHandle, folderNames = []) {
for await (const entry of directoryHandle.values()) {
try {
if (entry.kind === 'file' && entry.name.endsWith('.nfo')) {
yield {
fileHandle: entry,
folderNames: [...folderNames],
parentDirectoryHandle: directoryHandle
}
} else if (entry.kind === 'directory') {
yield* this.getFiles(entry, [...folderNames, entry.name])
}
} catch (e) {
console.error(e)
}
}
}
/**
* 查找视频文件名
* @param {FileSystemDirectoryHandle} directoryHandle - 当前目录句柄
* @returns {Promise<string>} 找到的视频文件名或空字符串
*/
async findVideoFileName(directoryHandle) {
for await (const entry of directoryHandle.values()) {
if (
entry.kind === 'file' &&
(entry.name.endsWith('.mp4') ||
entry.name.endsWith('.mkv') ||
entry.name.endsWith('.avi') ||
entry.name.endsWith('.flv') ||
entry.name.endsWith('.wmv') ||
entry.name.endsWith('.mov') ||
entry.name.endsWith('.rmvb'))
) {
return entry.name
}
}
return ''
}
/**
* 处理文件名
* 去掉 '.nfo'、'-c'、'-C' 和 '-破解' 后缀,并转换为小写
* @param {string} fileName - 原始文件名
* @returns {string} 处理后的文件名
*/
processFileName(fileName) {
let processedName = fileName.substring(
0,
fileName.length - '.nfo'.length
)
processedName = processedName.replace(/-c$/i, '')
processedName = processedName.replace(/-破解$/i, '')
return processedName.toLowerCase()
}
}
return function () {
new LocalFolderHandlerClass()
}
})()
/**
* 列表页处理函数
*/
const ListPageHandler = (function () {
/**
* @type {string} btsow 搜索 URL 基础路径
*/
const btsowUrl = 'https://btsow.com/search/'
/**
* 获取本地存储的 nfo 文件名的 JSON 字符串
* @returns {string[]|null} nfo 文件名数组或 null
*/
function getNfoFiles() {
const nfoFilesJson = localStorage.getItem('nfoFiles')
return nfoFilesJson ? JSON.parse(nfoFilesJson) : null
}
/**
* 创建本地打开视频所在文件夹按钮
* @param {HTMLElement} ele 要添加的所在的元素
*/
function createOpenLocalFolderBtn(ele) {
if (ele.querySelector('.open_local_folder')) {
return
}
const openLocalFolderBtnElement = document.createElement('div')
openLocalFolderBtnElement.className = 'tag open_local_folder'
openLocalFolderBtnElement.textContent = '本地打开'
Object.assign(openLocalFolderBtnElement.style, {
marginLeft: '10px',
color: '#fff',
backgroundColor: '#F8D714'
})
openLocalFolderBtnElement.addEventListener('click', function (event) {
event.preventDefault()
const localFolderPath = 'Z:\\日本'
// 打开本地文件夹逻辑
})
ele.querySelector('.tags').appendChild(openLocalFolderBtnElement)
}
/**
* 创建 btsow 搜索视频按钮
* @param {HTMLElement} ele 要添加的所在的元素
* @param {string} videoTitle 视频标题
*/
function createBtsowBtn(ele, videoTitle) {
if (ele.querySelector('.btsow')) {
return
}
const btsowBtnElement = document.createElement('div')
btsowBtnElement.className = 'tag btsow'
btsowBtnElement.textContent = 'Btsow'
Object.assign(btsowBtnElement.style, {
marginLeft: '10px',
color: '#fff',
backgroundColor: '#FF8400'
})
btsowBtnElement.addEventListener('click', function (event) {
event.preventDefault()
window.open(`${btsowUrl}${videoTitle}`, '_blank')
})
ele.querySelector('.tags').appendChild(btsowBtnElement)
}
/**
* 显示本地下载的文件名并改写样式
* @param {HTMLElement} ele 元素
* @param {Object} item 影片项
*/
function displayOperationOfTheItemInQuestion(ele, item) {
const imgElement = ele.querySelector('.cover img')
imgElement.style.padding = '10px'
imgElement.style.backgroundColor = '#FF0000'
const videoTitleElement = document.createElement('div')
videoTitleElement.textContent = item.originalFileName
Object.assign(videoTitleElement.style, {
margin: '1rem',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: '#fff',
fontSize: '.75rem',
height: '2rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '5px'
})
ele.querySelector('.box').appendChild(videoTitleElement)
videoTitleElement.addEventListener('click', function () {
navigator.clipboard.writeText(item.originalFileName)
videoTitleElement.textContent = item.originalFileName + ' 复制成功'
})
}
/**
* 处理列表页逻辑
*/
function handler() {
const nfoFilesArray = getNfoFiles()
if (!nfoFilesArray) {
return
}
LoadingGif.start()
$('.movie-list .item').each(function (index, ele) {
const videoTitle = ele.querySelector('strong').innerText.toLowerCase()
createBtsowBtn(ele, videoTitle)
nfoFilesArray.forEach(function (item) {
if (item.transformedName.includes(videoTitle)) {
createOpenLocalFolderBtn(ele)
displayOperationOfTheItemInQuestion(ele, item)
}
})
})
LoadingGif.stop()
}
return handler
})()
/**
* 详情页处理函数
*/
const DetailPageHandler = (function () {
/**
* 获取页面视频标题
* @returns {string} 视频标题文本
*/
function getVideoTitle() {
return $('.video-detail strong').first().text().trim().toLowerCase()
}
/**
* 从 localStorage 获取 nfoFiles
* @returns {Array} nfoFiles 数组
*/
function getNfoFiles() {
const nfoFilesJson = localStorage.getItem('nfoFiles')
return nfoFilesJson ? JSON.parse(nfoFilesJson) : null
}
/**
* 设置 .video-meta-panel 背景色
*/
function highlightVideoPanel() {
$('.video-meta-panel').css({ backgroundColor: '#FFC0CB' })
}
/**
* 创建或获取影片存在提示元素
* @returns {HTMLElement} localFolderTitleListElement 元素
*/
function createOrGetLocalFolderTitleListElement() {
let localFolderTitleListElement = document.querySelector(
'.localFolderTitleListElement'
)
if (!localFolderTitleListElement) {
localFolderTitleListElement = document.createElement('div')
localFolderTitleListElement.className = 'localFolderTitleListElement'
localFolderTitleListElement.textContent = 'Emby已存在影片'
Object.assign(localFolderTitleListElement.style, {
color: '#fff',
backgroundColor: '#FF8400',
padding: '5px 10px',
borderRadius: '5px',
fontSize: '16px',
fontWeight: 'bold',
position: 'fixed',
left: '20px',
top: '200px',
width: '240px'
})
document.body.appendChild(localFolderTitleListElement)
}
return localFolderTitleListElement
}
/**
* 添加影片列表项
* @param {Object} item 影片项
*/
function addLocalFolderTitleListItem(item) {
const localFolderTitleListElement =
createOrGetLocalFolderTitleListElement()
const localFolderTitleListItem = document.createElement('div')
localFolderTitleListItem.className = 'localFolderTitleListItem'
localFolderTitleListItem.textContent = item.originalFileName
Object.assign(localFolderTitleListItem.style, {
color: '#fff',
padding: '5px 10px',
borderRadius: '5px',
fontSize: '16px',
fontWeight: 'bold',
marginTop: '10px'
})
localFolderTitleListElement.appendChild(localFolderTitleListItem)
localFolderTitleListItem.addEventListener('click', function () {
navigator.clipboard.writeText(item.transformedName)
localFolderTitleListItem.textContent =
item.originalFileName + ' 复制成功'
})
}
/**
* 排序种子列表
*/
function sortBtList() {
const magnetsContent = document.getElementById('magnets-content')
if (!magnetsContent?.children.length) return
const items = Array.from(magnetsContent.querySelectorAll('.item'))
items.forEach(function (item) {
const metaSpan = item.querySelector('.meta')
if (metaSpan) {
const metaText = metaSpan.textContent.trim()
const match = metaText.match(/(\d+(\.\d+)?)GB/)
const size = match ? parseFloat(match[1]) : 0
item.dataset.size = size
}
})
items.sort(function (a, b) {
return b.dataset.size - a.dataset.size
})
const priority = {
high: [],
medium: [],
low: []
}
items.forEach(function (item) {
const nameSpan = item.querySelector('.name')
if (nameSpan) {
const nameText = nameSpan.textContent.trim()
if (/(-c| -C)/i.test(nameText)) {
priority.high.push(item)
item.style.backgroundColor = '#FFCCFF'
} else if (!/[A-Z]/.test(nameText)) {
priority.medium.push(item)
item.style.backgroundColor = '#FFFFCC'
} else {
priority.low.push(item)
}
}
})
magnetsContent.innerHTML = ''
priority.high.forEach(function (item) {
magnetsContent.appendChild(item)
})
priority.medium.forEach(function (item) {
magnetsContent.appendChild(item)
})
priority.low.forEach(function (item) {
magnetsContent.appendChild(item)
})
}
/**
* 主函数,处理详情页逻辑
*/
function handler() {
const videoTitle = getVideoTitle()
if (!videoTitle) return
const nfoFiles = getNfoFiles()
if (!nfoFiles) return
LoadingGif.start()
nfoFiles.forEach(function (item) {
if (item.transformedName.includes(videoTitle)) {
highlightVideoPanel()
addLocalFolderTitleListItem(item)
}
})
sortBtList()
LoadingGif.stop()
}
return handler
})()
/**
* 页面加载前执行
*/
async function onBeforeMount() {
// 立即调用以初始化按钮和事件处理程序
LocalFolderHandler()
// 调用列表页处理函数
ListPageHandler()
// 调用详情页处理函数
DetailPageHandler()
}
onBeforeMount()
})()