// ==UserScript==
// @name 简篇助手
// @namespace http://tampermonkey.net/
// @version 0.5
// @description 简篇网站账号切换与媒体提取工具
// @author Your name
// @match https://www.jianpian.cn/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const STYLES = {
floatingWindow: `
position: fixed;
top: 20px;
right: 20px;
width: 320px;
height: 500px;
background: rgba(255, 255, 255, 0.98);
border-radius: 12px;
padding: 15px;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
`,
tabs: `display: flex; margin-bottom: 15px; border-bottom: 1px solid #eee;`,
tab: `padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; color: #666; transition: all 0.3s ease;`,
activeTab: `color: #2c5282; border-bottom: 2px solid #2c5282;`,
button: `
background: #4299e1 !important;
color: white !important;
border: none !important;
padding: 8px 12px !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 13px !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
width: 100% !important;
height: auto !important;
line-height: 1.5 !important;
box-sizing: border-box !important;
margin: 0 !important;
text-align: center !important;
display: block !important;
`,
content: `height: 100%; overflow-y: auto; padding: 10px;`,
minimizeButton: `
position: absolute;
top: 15px;
right: 15px;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
cursor: pointer;
font-size: 16px;
color: #666;
transition: all 0.3s ease;
`,
minimized: `
position: fixed;
top: 20%;
right: 20px;
width: auto;
height: auto;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.98);
border-radius: 12px;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: all 0.3s ease;
`,
modal: `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`,
modalContent: `
background: white;
padding: 20px;
border-radius: 12px;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
`,
accountItem: `
padding: 12px;
margin: 8px 0;
border: 1px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
`
};
const ESSENTIAL_COOKIES = ['epian-token', 'epian-user-id'];
const MediaStore = {
items: new Map(),
clear() {
this.items.clear();
},
add(type, url, posterUrl = null) {
const processedUrl = type === '图片' ? reformatImageUrl(url) : url;
if (this.items.has(processedUrl)) return false;
this.items.set(processedUrl, {
type,
url: processedUrl,
posterUrl,
bbcode: this.generateBBCode(type, processedUrl, posterUrl)
});
return true;
},
generateBBCode(type, url, posterUrl = null) {
switch(type) {
case '图片':
return `[img]${url}[/img]`;
case '音频':
return `[audio]${url}[/audio]`;
case '视频':
return `[movie]${url}[/movie]`; // 基础BBCode不包含封面
}
},
getAllUrls() {
return Array.from(this.items.keys());
},
getAllBBCode() {
return Array.from(this.items.values()).map(item => {
// 普通BBCode永远不包含封面
return this.generateBBCode(item.type, item.url);
});
},
getAllBBCode2() {
return Array.from(this.items.values()).map(item => {
// BBCode2 仅在视频类型且有封面时才包含封面
if (item.type === '视频' && item.posterUrl) {
return `[movie]${item.url}|${item.posterUrl}[/movie]`;
}
return this.generateBBCode(item.type, item.url);
});
},
getItem(url) {
return this.items.get(url);
},
size() {
return this.items.size;
}
};
function isValidUrl(url) {
return url && (
url.startsWith('https://media-volc.jianpian.info/') ||
url.startsWith('https://img-volc.jianpian.info/')
);
}
function reformatImageUrl(url) {
if (!url.startsWith('https://img-volc.jianpian.info/')) return url;
const match = url.match(/https:\/\/img-volc\.jianpian\.info\/([^?~]+)/);
if (!match) return url;
const filePath = match[1];
if (filePath.includes('__transed__')) {
const [basePath, extension] = filePath.split('__transed__');
return `https://img-volc.jianpian.info/${basePath}.${extension.split('.')[0]}`;
}
return url;
}
function updateButtonState(button, originalText, duration = 1000) {
button.textContent = '已复制';
setTimeout(() => button.textContent = originalText, duration);
}
const MEDIA_STYLES = {
container: `
margin: 0 0 15px 0;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e2e8f0;
`,
typeLabel: (type) => `
display: inline-block;
padding: 2px 8px;
background: ${
type === '视频' ? '#3182ce' :
type === '音频' ? '#38a169' :
'#805ad5'
};
color: white;
border-radius: 4px;
font-size: 12px;
margin-bottom: 8px;
`,
url: `
font-size: 14px;
color: #4a5568;
word-break: break-all;
margin-bottom: 12px;
line-height: 1.5;
font-family: monospace;
`,
buttonGroup: `
display: flex;
gap: 10px;
`
};
function addMediaLink(url, type, posterUrl = null) {
if (!MediaStore.add(type, url, posterUrl)) return;
const container = document.getElementById('mediaContent');
const linkDiv = document.createElement('div');
const mediaItem = MediaStore.getItem(type === '图片' ? reformatImageUrl(url) : url);
linkDiv.style.cssText = MEDIA_STYLES.container;
linkDiv.innerHTML = `
<div style="${MEDIA_STYLES.typeLabel(type)}">${type}</div>
<div style="${MEDIA_STYLES.url}">${mediaItem.url}</div>
<div style="${MEDIA_STYLES.buttonGroup}">
<button class="copy-btn" style="${STYLES.button}">复制链接</button>
<button class="copy-bbcode-btn" style="${STYLES.button} background: #38a169 !important;">复制BBCode</button>
${type === '视频' && posterUrl ? `
<button class="copy-bbcode2-btn" style="${STYLES.button} background: #805ad5 !important;">复制BBCode2</button>
` : ''}
</div>
`;
const copyBtn = linkDiv.querySelector('.copy-btn');
const copyBBCodeBtn = linkDiv.querySelector('.copy-bbcode-btn');
copyBtn.onclick = () => {
navigator.clipboard.writeText(mediaItem.url);
updateButtonState(copyBtn, '复制链接');
};
copyBBCodeBtn.onclick = () => {
// 普通BBCode永远不包含封面
navigator.clipboard.writeText(MediaStore.generateBBCode(type, mediaItem.url));
updateButtonState(copyBBCodeBtn, '复制BBCode');
};
if (type === '视频' && posterUrl) {
const copyBBCode2Btn = linkDiv.querySelector('.copy-bbcode2-btn');
copyBBCode2Btn.onclick = () => {
// BBCode2 包含封面
navigator.clipboard.writeText(`[movie]${mediaItem.url}|${posterUrl}[/movie]`);
updateButtonState(copyBBCode2Btn, '复制BBCode2');
};
}
container.appendChild(linkDiv);
}
function createBatchCopyButtons() {
const container = document.createElement('div');
container.style.cssText = MEDIA_STYLES.buttonGroup;
container.style.padding = '10px';
container.style.marginBottom = '15px';
const buttons = [
['复制全部链接', () => MediaStore.getAllUrls()],
['复制全部BBCode', () => MediaStore.getAllBBCode()],
['复制全部BBCode2', () => MediaStore.getAllBBCode2()]
];
buttons.forEach(([text, getter]) => {
const button = document.createElement('button');
button.textContent = text;
button.style.cssText = STYLES.button;
button.onclick = () => {
navigator.clipboard.writeText(getter().join('\n'));
updateButtonState(button, text);
};
container.appendChild(button);
});
return container;
}
const POSTER_URL_REGEX = /url\("([^"]+)"\)/;
const MediaScanner = {
scanVideoPosters() {
const posterMap = new Map();
document.querySelectorAll('.poster').forEach(poster => {
const style = poster.getAttribute('style');
if (style) {
const match = style.match(POSTER_URL_REGEX);
if (match && isValidUrl(match[1])) {
const videoElem = poster.closest('div[class*="video"]')?.querySelector('video');
if (videoElem) {
const posterUrl = reformatImageUrl(match[1]);
posterMap.set(videoElem, posterUrl);
}
}
}
});
return posterMap;
},
scan() {
try {
const posterMap = this.scanVideoPosters();
// 扫描视频
document.querySelectorAll('video').forEach(video => {
if (video.src && isValidUrl(video.src)) {
addMediaLink(video.src, '视频', posterMap.get(video));
}
video.querySelectorAll('source').forEach(source => {
if (source.src && isValidUrl(source.src)) {
addMediaLink(source.src, '视频', posterMap.get(video));
}
});
});
// 扫描音频
document.querySelectorAll('audio').forEach(audio => {
if (audio.src && isValidUrl(audio.src)) {
addMediaLink(audio.src, '音频');
}
audio.querySelectorAll('source').forEach(source => {
if (source.src && isValidUrl(source.src)) {
addMediaLink(source.src, '音频');
}
});
});
// 扫描图片
document.querySelectorAll('img').forEach(img => {
if (img.src && isValidUrl(img.src)) {
addMediaLink(img.src, '图片');
}
});
return MediaStore.size();
} catch (error) {
console.error('扫描媒体出错:', error);
throw error;
}
}
};
function scanMedia() {
const mediaContent = document.getElementById('mediaContent');
if (!mediaContent || mediaContent.style.display === 'none') return;
mediaContent.innerHTML = '';
mediaContent.appendChild(createBatchCopyButtons());
MediaStore.clear();
requestAnimationFrame(() => {
try {
const count = MediaScanner.scan();
if (count === 0) {
mediaContent.innerHTML = '<div style="text-align: center; padding: 30px;">未找到媒体文件</div>';
}
} catch (error) {
mediaContent.innerHTML = '扫描媒体时出错,请刷新页面重试';
}
});
}
function getAccountList(accounts) {
const accountNames = Object.keys(accounts);
if (accountNames.length === 0) return null;
return '已保存的账号:\n\n' + accountNames.map(name => {
const account = accounts[name];
return `${name}${account.note ? ` (${account.note})` : ''}\n保存时间: ${account.saveDate}`;
}).join('\n\n');
}
function showAccountSelector(accounts, onSelect, title) {
const modal = document.createElement('div');
modal.style.cssText = STYLES.modal;
const content = document.createElement('div');
content.style.cssText = STYLES.modalContent;
content.innerHTML = `<h3 style="margin: 0 0 16px 0; font-size: 18px;">${title}</h3>`;
Object.entries(accounts).forEach(([name, account]) => {
const item = document.createElement('div');
item.style.cssText = STYLES.accountItem;
item.innerHTML = `
<div style="font-weight: 500; color: #2d3748;">${name}</div>
${account.note ? `<div style="color: #718096; font-size: 12px;">备注: ${account.note}</div>` : ''}
<div style="color: #a0aec0; font-size: 12px;">保存时间: ${account.saveDate}</div>
`;
item.onmouseover = () => item.style.background = '#f7fafc';
item.onmouseout = () => item.style.background = 'white';
item.onclick = () => {
onSelect(name);
document.body.removeChild(modal);
};
content.appendChild(item);
});
const closeBtn = document.createElement('button');
closeBtn.textContent = '取消';
closeBtn.style.cssText = STYLES.button;
closeBtn.onclick = () => document.body.removeChild(modal);
content.appendChild(closeBtn);
modal.appendChild(content);
document.body.appendChild(modal);
// 点击背景关闭
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
};
}
function checkAccounts() {
const accounts = GM_getValue('accounts', {});
if (Object.keys(accounts).length === 0) {
alert('还没有保存任何账号!');
return null;
}
return accounts;
}
function switchAccount() {
const accounts = checkAccounts();
if (!accounts) return;
showAccountSelector(accounts, (selectedAccount) => {
ESSENTIAL_COOKIES.forEach(name => {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.jianpian.cn`;
});
accounts[selectedAccount].cookies.split(';').forEach(cookie => {
document.cookie = `${cookie.trim()}; path=/; domain=.jianpian.cn`;
});
location.reload();
}, '选择要切换的账号');
}
function deleteAccount() {
const accounts = checkAccounts();
if (!accounts) return;
showAccountSelector(accounts, (selectedAccount) => {
if (confirm(`确定要删除账号 "${selectedAccount}" 吗?`)) {
delete accounts[selectedAccount];
GM_setValue('accounts', accounts);
alert('删除成功!');
}
}, '选择要删除的账号');
}
const BUTTON_CONFIGS = {
saveCookie: ['保存当前账号', async () => {
const currentCookies = document.cookie.split(';');
const essentialCookies = currentCookies.filter(cookie =>
ESSENTIAL_COOKIES.some(name => cookie.split('=')[0].trim() === name)
);
if (essentialCookies.length === 0) {
alert('未检测到登录状态,请先登录!');
return;
}
const accountName = prompt('请为当前账号设置一个名称:');
if (!accountName) return;
const accountNote = prompt('请输入账号备注(可):');
const accounts = GM_getValue('accounts', {});
accounts[accountName] = {
cookies: essentialCookies.join(';'),
note: accountNote || '',
saveDate: new Date().toLocaleString()
};
GM_setValue('accounts', accounts);
alert('保存成功!');
}],
switchAccount: ['切换账号', switchAccount],
deleteAccount: ['删除账号', deleteAccount],
exportAccounts: ['导出账号', () => {
const accounts = GM_getValue('accounts', {});
const blob = new Blob([JSON.stringify(accounts, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = '简篇账号数据.json';
a.click();
URL.revokeObjectURL(a.href);
}],
importAccounts: ['导入账号', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = e => {
const reader = new FileReader();
reader.onload = event => {
try {
const accounts = JSON.parse(event.target.result);
GM_setValue('accounts', accounts);
alert('导入成功!');
} catch (error) {
alert('导入失败,文件格式错误!');
}
};
reader.readAsText(e.target.files[0]);
};
input.click();
}]
};
function createUI() {
const container = document.createElement('div');
container.id = 'jianpianHelper';
container.style.cssText = STYLES.floatingWindow;
const minimizeBtn = document.createElement('div');
minimizeBtn.textContent = '−';
minimizeBtn.style.cssText = STYLES.minimizeButton;
minimizeBtn.title = '最小化';
let isMinimized = false;
minimizeBtn.onclick = (e) => {
e.stopPropagation();
isMinimized = !isMinimized;
if (isMinimized) {
container.innerHTML = '简篇助手';
container.style.cssText = STYLES.minimized;
container.title = '点击展开';
container.onclick = () => {
isMinimized = false;
container.style.cssText = STYLES.floatingWindow;
container.innerHTML = '';
setupUI();
setupAccountButtons();
container.onclick = null;
};
}
};
function setupUI() {
const tabs = document.createElement('div');
tabs.style.cssText = STYLES.tabs;
const [accountTab, mediaTab] = ['账号管理', '媒体提取'].map(text => {
const tab = document.createElement('div');
tab.textContent = text;
tab.style.cssText = STYLES.tab + (text === '账号管理' ? STYLES.activeTab : '');
return tab;
});
tabs.append(accountTab, mediaTab);
const [accountContent, mediaContent] = ['account', 'media'].map(id => {
const content = document.createElement('div');
content.id = `${id}Content`;
content.style.cssText = STYLES.content + `; display: ${id === 'account' ? 'block' : 'none'};`;
return content;
});
[accountTab, mediaTab].forEach((tab, i) => {
tab.onclick = () => {
[accountTab, mediaTab].forEach((t, j) =>
t.style.cssText = STYLES.tab + (i === j ? STYLES.activeTab : '')
);
accountContent.style.display = i === 0 ? 'block' : 'none';
mediaContent.style.display = i === 0 ? 'none' : 'block';
if (i === 1) scanMedia();
};
});
container.appendChild(minimizeBtn);
container.appendChild(tabs);
container.appendChild(accountContent);
container.appendChild(mediaContent);
}
setupUI();
return container;
}
function setupAccountButtons() {
const accountContent = document.getElementById('accountContent');
const accountButtons = document.createElement('div');
accountButtons.style.cssText = `
padding: 10px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
`;
Object.entries(BUTTON_CONFIGS).forEach(([_, [text, handler]]) => {
const button = document.createElement('button');
button.textContent = text;
button.style.cssText = STYLES.button;
button.onclick = handler;
accountButtons.appendChild(button);
});
accountContent.appendChild(accountButtons);
}
function init() {
const container = createUI();
document.body.appendChild(container);
setupAccountButtons();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();